diff --git a/Classes/GraphQL/MediaApi.php b/Classes/GraphQL/MediaApi.php index d0c6e04f3..e488b3e7d 100644 --- a/Classes/GraphQL/MediaApi.php +++ b/Classes/GraphQL/MediaApi.php @@ -319,52 +319,37 @@ public function updateAsset( string $label = null, string $caption = null, string $copyrightNotice = null - ): ?Types\Asset { + ): MutationResult { return $this->assetMutator->updateAsset($id, $assetSourceId, $label, $caption, $copyrightNotice); } - /** - * @throws MediaUiException - */ #[Mutation] - public function tagAsset(Types\AssetId $id, Types\AssetSourceId $assetSourceId, Types\TagId $tagId): ?Types\Asset + public function tagAsset(Types\AssetId $id, Types\AssetSourceId $assetSourceId, Types\TagId $tagId): MutationResult { return $this->assetMutator->tagAsset($id, $assetSourceId, $tagId); } - /** - * @throws MediaUiException - */ #[Mutation] - public function untagAsset(Types\AssetId $id, Types\AssetSourceId $assetSourceId, Types\TagId $tagId): ?Types\Asset + public function untagAsset(Types\AssetId $id, Types\AssetSourceId $assetSourceId, Types\TagId $tagId): MutationResult { return $this->assetMutator->untagAsset($id, $assetSourceId, $tagId); } - /** - * @throws MediaUiException - */ #[Mutation] public function deleteAsset(Types\AssetId $id, Types\AssetSourceId $assetSourceId): MutationResult { return $this->assetMutator->deleteAsset($id, $assetSourceId); } - /** - * @throws MediaUiException - */ #[Mutation] public function setAssetTags( Types\AssetId $id, Types\AssetSourceId $assetSourceId, Types\TagIds $tagIds - ): ?Types\Asset { + ): MutationResult { return $this->assetMutator->setAssetTags($id, $assetSourceId, $tagIds); } - /** - * @throws MediaUiException - */ #[Mutation] public function setAssetCollections( Types\AssetId $id, @@ -374,6 +359,45 @@ public function setAssetCollections( return $this->assetMutator->setAssetCollections($id, $assetSourceId, $assetCollectionIds); } + #[Mutation] + #[Description('Delete multiple assets at once')] + public function deleteAssets(Types\AssetIdentities $identities): Types\MutationResults + { + return $this->assetMutator->deleteAssets($identities); + } + + #[Mutation] + #[Description('Add a tag to multiple assets at once')] + public function tagAssets(Types\AssetIdentities $identities, Types\TagId $tagId): Types\MutationResults + { + return $this->assetMutator->tagAssets($identities, $tagId); + } + + #[Mutation] + #[Description('Remove a tag from multiple assets at once')] + public function untagAssets(Types\AssetIdentities $identities, Types\TagId $tagId): Types\MutationResults + { + return $this->assetMutator->untagAssets($identities, $tagId); + } + + #[Mutation] + #[Description('Assign multiple assets to a collection')] + public function assignAssetsToCollection( + Types\AssetIdentities $identities, + Types\AssetCollectionId $assetCollectionId + ): Types\MutationResults { + return $this->assetMutator->assignAssetsToCollection($identities, $assetCollectionId); + } + + #[Mutation] + #[Description('Update properties of multiple assets at once')] + public function updateAssets( + Types\AssetIdentities $identities, + string $copyrightNotice = null + ): Types\MutationResults { + return $this->assetMutator->updateAssets($identities, $copyrightNotice); + } + /** * @throws IllegalObjectTypeException */ diff --git a/Classes/GraphQL/Mutator/AssetMutator.php b/Classes/GraphQL/Mutator/AssetMutator.php index ab5f276c3..486bd687f 100644 --- a/Classes/GraphQL/Mutator/AssetMutator.php +++ b/Classes/GraphQL/Mutator/AssetMutator.php @@ -85,6 +85,34 @@ protected function localizedMessageFromException(\Exception $exception): string return $this->localizedMessage($labelIdentifier, $exception->getMessage()); } + /** + * @throws MediaUiException + */ + protected function resolveAsset(Types\AssetId $id, Types\AssetSourceId $assetSourceId, string $action): Asset + { + $asset = $this->assetSourceContext->getAsset($id, $assetSourceId); + if (!$asset) { + throw new MediaUiException("Cannot $action asset that was never imported", 1590659044); + } + if (!$asset instanceof Asset) { + throw new MediaUiException("Asset type does not support \"$action\" action", 1619081662); + } + return $asset; + } + + /** + * @throws MediaUiException + */ + protected function resolveTag(Types\TagId $tagId): Tag + { + /** @var Tag|null $tag */ + $tag = $this->tagRepository->findByIdentifier($tagId->value); + if (!$tag) { + throw new MediaUiException('Cannot resolve tag that does not exist', 1591561845); + } + return $tag; + } + /** * @throws MediaUiException */ @@ -94,7 +122,7 @@ public function updateAsset( string $label = null, string $caption = null, string $copyrightNotice = null - ): ?Types\Asset { + ): MutationResult { $asset = $this->assetSourceContext->getAsset($id, $assetSourceId); if (!$asset) { throw new MediaUiException('Cannot update asset that was never imported', 1590659044); @@ -119,28 +147,16 @@ public function updateAsset( throw new MediaUiException('Failed to update asset: ' . $e->getMessage(), 1590659063); } - return Types\Asset::fromAssetProxy($asset->getAssetProxy()); + return MutationResult::fromSuccess(); } /** * @throws MediaUiException */ - public function tagAsset(Types\AssetId $id, Types\AssetSourceId $assetSourceId, Types\TagId $tagId): ?Types\Asset + public function tagAsset(Types\AssetId $id, Types\AssetSourceId $assetSourceId, Types\TagId $tagId): MutationResult { - $asset = $this->assetSourceContext->getAsset($id, $assetSourceId); - if (!$asset) { - throw new MediaUiException('Cannot tag asset that was never imported', 1591561758); - } - - if (!$asset instanceof Asset) { - throw new MediaUiException('Asset type does not support tagging', 1619081662); - } - - /** @var Tag $tag */ - $tag = $this->tagRepository->findByIdentifier($tagId->value); - if (!$tag) { - throw new MediaUiException('Cannot tag asset with tag that does not exist', 1591561845); - } + $asset = $this->resolveAsset($id, $assetSourceId, 'tag'); + $tag = $this->resolveTag($tagId); $asset->addTag($tag); @@ -150,7 +166,7 @@ public function tagAsset(Types\AssetId $id, Types\AssetSourceId $assetSourceId, $this->logger->error('Failed to update asset', [$e->getMessage()]); throw new MediaUiException('Failed to update asset', 1591561868); } - return Types\Asset::fromAssetProxy($asset->getAssetProxy()); + return MutationResult::fromSuccess(); } /** @@ -201,25 +217,12 @@ public function setAssetTags( Types\AssetId $id, Types\AssetSourceId $assetSourceId, Types\TagIds $tagIds - ): ?Types\Asset { - $asset = $this->assetSourceContext->getAsset($id, $assetSourceId); - if (!$asset) { - throw new MediaUiException('Cannot tag asset that was never imported', 1594621322); - } - if (!$asset instanceof Asset) { - throw new MediaUiException( - sprintf('Asset type %s does not support tagging', $asset::class), - 1619081714 - ); - } + ): MutationResult { + $asset = $this->resolveAsset($id, $assetSourceId, 'tag'); $tags = new ArrayCollection(); foreach ($tagIds as $tagId) { - $tag = $this->tagRepository->findByIdentifier($tagId->value); - if (!$tag) { - throw new MediaUiException('Cannot tag asset with tag that does not exist', 1594621318); - } - $tags->add($tag); + $tags->add($this->resolveTag($tagId)); } $asset->setTags($tags); @@ -229,7 +232,7 @@ public function setAssetTags( throw new MediaUiException('Failed to set asset tags: ' . $e->getMessage(), 1594621296); } - return Types\Asset::fromAssetProxy($asset->getAssetProxy()); + return MutationResult::fromSuccess(); } /** @@ -240,19 +243,13 @@ public function setAssetCollections( Types\AssetSourceId $assetSourceId, Types\AssetCollectionIds $assetCollectionIds ): MutationResult { - $asset = $this->assetSourceContext->getAsset($id, $assetSourceId); - if (!$asset) { - throw new MediaUiException('Cannot assign collections to asset that was never imported', 1594621322); - } - if (!$asset instanceof Asset) { - throw new MediaUiException('Asset type does not support collections', 1619081722); - } + $asset = $this->resolveAsset($id, $assetSourceId, 'assign collections to'); $assetCollections = new ArrayCollection(); foreach ($assetCollectionIds as $assetCollectionId) { $collection = $this->assetCollectionRepository->findByIdentifier($assetCollectionId->value); if (!$collection) { - throw new MediaUiException('Cannot assign non existing assign collection to asset', 1594621318); + throw new MediaUiException('Cannot assign non existing collection to asset', 1594621318); } $assetCollections->add($collection); } @@ -270,21 +267,10 @@ public function setAssetCollections( /** * @throws MediaUiException */ - public function untagAsset(Types\AssetId $id, Types\AssetSourceId $assetSourceId, Types\TagId $tagId): ?Types\Asset + public function untagAsset(Types\AssetId $id, Types\AssetSourceId $assetSourceId, Types\TagId $tagId): MutationResult { - $asset = $this->assetSourceContext->getAsset($id, $assetSourceId); - if (!$asset) { - throw new MediaUiException('Cannot untag asset that was never imported', 1591561930); - } - if (!$asset instanceof Asset) { - throw new MediaUiException('Asset type does not support tagging', 1619081740); - } - - /** @var Tag $tag */ - $tag = $this->tagRepository->findByIdentifier($tagId->value); - if (!$tag) { - throw new MediaUiException('Cannot untag asset from tag that does not exist', 1591561934); - } + $asset = $this->resolveAsset($id, $assetSourceId, 'untag'); + $tag = $this->resolveTag($tagId); $asset->removeTag($tag); @@ -294,7 +280,110 @@ public function untagAsset(Types\AssetId $id, Types\AssetSourceId $assetSourceId throw new MediaUiException('Failed to update asset: ' . $e->getMessage(), 1591561938); } - return Types\Asset::fromAssetProxy($asset->getAssetProxy()); + return MutationResult::fromSuccess(); + } + + public function deleteAssets(Types\AssetIdentities $identities): Types\MutationResults + { + $results = []; + foreach ($identities as $identity) { + try { + $results[] = $this->deleteAsset($identity->assetId, $identity->assetSourceId); + } catch (\Exception $e) { + $results[] = MutationResult::fromError([$e->getMessage()]); + } + } + return Types\MutationResults::fromArray($results); + } + + public function tagAssets(Types\AssetIdentities $identities, Types\TagId $tagId): Types\MutationResults + { + try { + $tag = $this->resolveTag($tagId); + } catch (MediaUiException $e) { + return Types\MutationResults::fromArray([MutationResult::fromError([$e->getMessage()])]); + } + + $results = []; + foreach ($identities as $identity) { + try { + $asset = $this->resolveAsset($identity->assetId, $identity->assetSourceId, 'tag'); + $asset->addTag($tag); + $this->assetRepository->update($asset); + $results[] = MutationResult::fromSuccess(); + } catch (\Exception $e) { + $results[] = MutationResult::fromError([$e->getMessage()]); + } + } + return Types\MutationResults::fromArray($results); + } + + public function untagAssets(Types\AssetIdentities $identities, Types\TagId $tagId): Types\MutationResults + { + try { + $tag = $this->resolveTag($tagId); + } catch (MediaUiException $e) { + return Types\MutationResults::fromArray([MutationResult::fromError([$e->getMessage()])]); + } + + $results = []; + foreach ($identities as $identity) { + try { + $asset = $this->resolveAsset($identity->assetId, $identity->assetSourceId, 'untag'); + $asset->removeTag($tag); + $this->assetRepository->update($asset); + $results[] = MutationResult::fromSuccess(); + } catch (\Exception $e) { + $results[] = MutationResult::fromError([$e->getMessage()]); + } + } + return Types\MutationResults::fromArray($results); + } + + public function assignAssetsToCollection( + Types\AssetIdentities $identities, + Types\AssetCollectionId $assetCollectionId + ): Types\MutationResults { + /** @var AssetCollection|null $collection */ + $collection = $this->assetCollectionRepository->findByIdentifier($assetCollectionId->value); + if (!$collection) { + return Types\MutationResults::fromArray([ + MutationResult::fromError([ + $this->localizedMessage('errors.collectionNotFound', 'Asset collection does not exist') + ]) + ]); + } + + $assetCollections = new ArrayCollection([$collection]); + + $results = []; + foreach ($identities as $identity) { + try { + $asset = $this->resolveAsset($identity->assetId, $identity->assetSourceId, 'assign collection to'); + $asset->setAssetCollections(clone $assetCollections); + $this->assetRepository->update($asset); + $results[] = MutationResult::fromSuccess(); + } catch (\Exception $e) { + $results[] = MutationResult::fromError([$e->getMessage()]); + } + } + return Types\MutationResults::fromArray($results); + } + + public function updateAssets( + Types\AssetIdentities $identities, + string $copyrightNotice = null + ): Types\MutationResults { + $results = []; + foreach ($identities as $identity) { + try { + $this->updateAsset($identity->assetId, $identity->assetSourceId, null, null, $copyrightNotice); + $results[] = MutationResult::fromSuccess(); + } catch (\Exception $e) { + $results[] = MutationResult::fromError([$e->getMessage()]); + } + } + return Types\MutationResults::fromArray($results); } /** @@ -308,13 +397,7 @@ public function replaceAsset( Types\UploadedFile $file, Types\AssetReplacementOptions $options, ): Types\FileUploadResult { - $asset = $this->assetSourceContext->getAsset($id, $assetSourceId); - if (!$asset) { - throw new MediaUiException('Cannot replace asset that was never imported', 1648046173); - } - if (!$asset instanceof Asset) { - throw new MediaUiException('Asset type "' . $asset::class . '" does not support replacing', 1648046186); - } + $asset = $this->resolveAsset($id, $assetSourceId, 'replace'); $sourceMediaType = MediaTypes::parseMediaType($asset->getMediaType()); $replacementMediaType = MediaTypes::parseMediaType($file->clientMediaType); @@ -384,13 +467,7 @@ public function editAsset( throw new MediaUiException('Filename was empty', 1678156902); } - $asset = $this->assetSourceContext->getAsset($id, $assetSourceId); - if (!$asset) { - throw new MediaUiException('Cannot rename asset that was never imported', 1678155884); - } - if (!$asset instanceof Asset) { - throw new MediaUiException('Asset type does not support renaming', 1678155887); - } + $asset = $this->resolveAsset($id, $assetSourceId, 'rename'); // Make sure the filename has the same extension as before if (!strpos($filename->value, $asset->getFileExtension())) { diff --git a/Classes/GraphQL/Resolver.php b/Classes/GraphQL/Resolver.php index cd8757b8b..a1fc3ec1c 100644 --- a/Classes/GraphQL/Resolver.php +++ b/Classes/GraphQL/Resolver.php @@ -41,6 +41,17 @@ class Resolver { private readonly CustomResolvers $customResolvers; + /** + * Mapping of type names that cannot be resolved by the default pluralization + * (appending 's') to their actual class names. For example, 'AssetIdentity' + 's' + * yields 'AssetIdentitys' but the actual class is 'AssetIdentities'. + * + * @var array + */ + private const array TYPE_ALIASES = [ + 'AssetIdentitys' => 'AssetIdentities', + ]; + /** * @param array $typeNamespaces */ @@ -163,6 +174,7 @@ private function convertArgument( */ private function resolveClassName(string $argumentType): ?string { + $argumentType = self::TYPE_ALIASES[$argumentType] ?? $argumentType; foreach ($this->typeNamespaces as $namespace) { $className = rtrim($namespace, '\\') . '\\' . $argumentType; if (class_exists($className)) { diff --git a/Classes/GraphQL/Types/AssetIdentities.php b/Classes/GraphQL/Types/AssetIdentities.php index a79deea84..ec51ecca8 100644 --- a/Classes/GraphQL/Types/AssetIdentities.php +++ b/Classes/GraphQL/Types/AssetIdentities.php @@ -14,9 +14,9 @@ final class AssetIdentities implements \IteratorAggregate { /** - * @param AssetIdentity[] $collections + * @param AssetIdentity[] $identities */ - private function __construct(public readonly array $collections) + private function __construct(public readonly array $identities) { } @@ -33,7 +33,7 @@ public static function fromArray(array $assetIdentities): self */ public function getIterator(): \Traversable { - yield from $this->collections; + yield from $this->identities; } public static function empty(): self diff --git a/Classes/GraphQL/Types/MutationResults.php b/Classes/GraphQL/Types/MutationResults.php new file mode 100644 index 000000000..6ddf2a2f0 --- /dev/null +++ b/Classes/GraphQL/Types/MutationResults.php @@ -0,0 +1,51 @@ + + */ + public function getIterator(): \Traversable + { + yield from $this->values; + } + + public static function empty(): self + { + return new self([]); + } + + /** + * @return MutationResult[] + */ + public function jsonSerialize(): array + { + return $this->values; + } +} diff --git a/Resources/Private/GraphQL/schema.root.graphql b/Resources/Private/GraphQL/schema.root.graphql index 9ad1d75bb..74818c3da 100644 --- a/Resources/Private/GraphQL/schema.root.graphql +++ b/Resources/Private/GraphQL/schema.root.graphql @@ -38,12 +38,22 @@ type Query { } type Mutation { - updateAsset(id: AssetId! assetSourceId: AssetSourceId! label: String caption: String copyrightNotice: String): Asset - tagAsset(id: AssetId! assetSourceId: AssetSourceId! tagId: TagId!): Asset - untagAsset(id: AssetId! assetSourceId: AssetSourceId! tagId: TagId!): Asset + updateAsset(id: AssetId! assetSourceId: AssetSourceId! label: String caption: String copyrightNotice: String): MutationResult! + tagAsset(id: AssetId! assetSourceId: AssetSourceId! tagId: TagId!): MutationResult! + untagAsset(id: AssetId! assetSourceId: AssetSourceId! tagId: TagId!): MutationResult! deleteAsset(id: AssetId! assetSourceId: AssetSourceId!): MutationResult! - setAssetTags(id: AssetId! assetSourceId: AssetSourceId! tagIds: [TagId!]!): Asset + setAssetTags(id: AssetId! assetSourceId: AssetSourceId! tagIds: [TagId!]!): MutationResult! setAssetCollections(id: AssetId! assetSourceId: AssetSourceId! assetCollectionIds: [AssetCollectionId!]!): MutationResult! + """ Delete multiple assets at once """ + deleteAssets(identities: [AssetIdentityInput!]!): [MutationResult!]! + """ Add a tag to multiple assets at once """ + tagAssets(identities: [AssetIdentityInput!]! tagId: TagId!): [MutationResult!]! + """ Remove a tag from multiple assets at once """ + untagAssets(identities: [AssetIdentityInput!]! tagId: TagId!): [MutationResult!]! + """ Assign multiple assets to a collection """ + assignAssetsToCollection(identities: [AssetIdentityInput!]! assetCollectionId: AssetCollectionId!): [MutationResult!]! + """ Update properties of multiple assets at once """ + updateAssets(identities: [AssetIdentityInput!]! copyrightNotice: String): [MutationResult!]! createAssetCollection(title: AssetCollectionTitle! assetSourceId: AssetSourceId! parent: AssetCollectionId): AssetCollection deleteAssetCollection(id: AssetCollectionId! assetSourceId: AssetSourceId!): MutationResult! updateAssetCollection(id: AssetCollectionId! assetSourceId: AssetSourceId! title: AssetCollectionTitle tagIds: [TagId!]): MutationResult! @@ -361,6 +371,13 @@ type MutationResult { messages: [MutationResponseMessage!] } +input AssetIdentityInput { + """ Unique identifier (UUID) of an Asset """ + assetId: AssetId! + """ Unique identifier of an Asset source (e.g. "neos") """ + assetSourceId: AssetSourceId! +} + input UploadedFileInput { size: Int! errorStatus: Int! diff --git a/Tests/Functional/GraphQL/AssetApiTest.php b/Tests/Functional/GraphQL/AssetApiTest.php index 92a335406..c75bfc773 100644 --- a/Tests/Functional/GraphQL/AssetApiTest.php +++ b/Tests/Functional/GraphQL/AssetApiTest.php @@ -175,7 +175,7 @@ public function testUpdateAsset(): void $asset = $assets->assets[0]; $this->assertEquals($file->clientFilename, $asset->filename->value); - $updatedAsset = $this->mediaApi->updateAsset( + $updateResult = $this->mediaApi->updateAsset( $asset->id, $asset->assetSource->id, 'some label', @@ -183,6 +183,9 @@ public function testUpdateAsset(): void 'copyright notice', ); + $this->assertTrue($updateResult->success); + + $updatedAsset = $this->mediaApi->asset($asset->id, $asset->assetSource->id); $this->assertEquals($asset->id, $updatedAsset->id); $this->assertEquals('some label', $this->assetResolver->label($updatedAsset)); $this->assertEquals('some caption', $this->assetResolver->caption($updatedAsset)); diff --git a/packages/core/src/hooks/index.ts b/packages/core/src/hooks/index.ts index 848743e1e..58e7e5ffa 100644 --- a/packages/core/src/hooks/index.ts +++ b/packages/core/src/hooks/index.ts @@ -2,7 +2,9 @@ export { default as useAssetCountQuery } from './useAssetCountQuery'; export { default as useAssetQuery } from './useAssetQuery'; export { default as useAssetsQuery } from './useAssetsQuery'; export { default as useConfigQuery } from './useConfigQuery'; +export { default as useAssignAssetsToCollection } from './useAssignAssetsToCollection'; export { default as useDeleteAsset } from './useDeleteAsset'; +export { default as useDeleteAssets } from './useDeleteAssets'; export { default as useDownloadAssets } from './useDownloadAssets'; export { default as useEvent } from './useEvent'; export { default as useImportAsset } from './useImportAsset'; @@ -11,3 +13,4 @@ export { default as useSelectedAsset } from './useSelectedAsset'; export { default as useSetAssetCollections } from './useSetAssetCollections'; export { default as useSetAssetTags } from './useSetAssetTags'; export { default as useUpdateAsset } from './useUpdateAsset'; +export { default as useUpdateAssets } from './useUpdateAssets'; diff --git a/packages/core/src/hooks/useAssignAssetsToCollection.ts b/packages/core/src/hooks/useAssignAssetsToCollection.ts new file mode 100644 index 000000000..9014217ec --- /dev/null +++ b/packages/core/src/hooks/useAssignAssetsToCollection.ts @@ -0,0 +1,26 @@ +import { useMutation } from '@apollo/client'; + +import { ASSIGN_ASSETS_TO_COLLECTION } from '../mutations'; + +interface AssignAssetsToCollectionVariables { + identities: { assetId: string; assetSourceId: string }[]; + assetCollectionId: string; +} + +export default function useAssignAssetsToCollection() { + const [action, { error, data, loading }] = useMutation< + { assignAssetsToCollection: MutationResult[] }, + AssignAssetsToCollectionVariables + >(ASSIGN_ASSETS_TO_COLLECTION); + + const assignAssetsToCollection = (identities: AssetIdentity[], assetCollectionId: string) => + action({ + variables: { + identities: identities.map(({ assetId, assetSourceId }) => ({ assetId, assetSourceId })), + assetCollectionId, + }, + refetchQueries: ['ASSETS', 'ASSET_COLLECTIONS'], + }).then(({ data }) => data.assignAssetsToCollection); + + return { assignAssetsToCollection, data, error, loading }; +} diff --git a/packages/core/src/hooks/useDeleteAssets.ts b/packages/core/src/hooks/useDeleteAssets.ts new file mode 100644 index 000000000..0a507d145 --- /dev/null +++ b/packages/core/src/hooks/useDeleteAssets.ts @@ -0,0 +1,47 @@ +import { useMutation } from '@apollo/client'; +import { useSetRecoilState } from 'recoil'; + +import { DELETE_ASSETS } from '../mutations'; +import { selectedAssetIdState } from '../state'; +import { useEvent } from './index'; +import { assetRemovedEvent } from '../events'; + +interface DeleteAssetsVariables { + identities: { assetId: string; assetSourceId: string }[]; +} + +export default function useDeleteAssets() { + const [action, { error, data, loading }] = useMutation<{ deleteAssets: MutationResult[] }, DeleteAssetsVariables>( + DELETE_ASSETS + ); + const setSelectedAsset = useSetRecoilState(selectedAssetIdState); + const assetRemoved = useEvent(assetRemovedEvent); + + const deleteAssets = (identities: AssetIdentity[]) => + action({ + variables: { + identities: identities.map(({ assetId, assetSourceId }) => ({ assetId, assetSourceId })), + }, + refetchQueries: ['ASSET_COLLECTIONS'], + update: (cache, { data }) => { + if (!data) return; + data.deleteAssets.forEach((result, index) => { + if (!result.success) return; + const { assetId, assetSourceId } = identities[index]; + cache.evict({ id: cache.identify({ __typename: 'Asset', id: assetId }) }); + assetRemoved({ assetId, assetSourceId }); + }); + cache.gc(); + }, + }).then(({ data }) => { + // Unselect if a selected asset was deleted + setSelectedAsset((prev) => { + if (!prev) return prev; + const deletedSuccessfully = identities.filter((_, i) => data.deleteAssets[i].success); + return deletedSuccessfully.some(({ assetId }) => assetId === prev.assetId) ? null : prev; + }); + return data.deleteAssets; + }); + + return { deleteAssets, data, error, loading }; +} diff --git a/packages/core/src/hooks/useSetAssetTags.ts b/packages/core/src/hooks/useSetAssetTags.ts index 2c187c5ca..f5d997a09 100644 --- a/packages/core/src/hooks/useSetAssetTags.ts +++ b/packages/core/src/hooks/useSetAssetTags.ts @@ -14,10 +14,9 @@ interface SetAssetTagsVariables { } export default function useSetAssetTags() { - const [action, { error, data, loading }] = useMutation< - { __typename: string; setAssetTags: Asset }, - SetAssetTagsVariables - >(SET_ASSET_TAGS); + const [action, { error, data, loading }] = useMutation<{ setAssetTags: MutationResult }, SetAssetTagsVariables>( + SET_ASSET_TAGS + ); const setAssetTags = ({ asset, tags }: SetAssetTagsProps) => action({ @@ -26,16 +25,11 @@ export default function useSetAssetTags() { assetSourceId: asset.assetSource.id, tagIds: tags.map((tag) => tag.id), }, - optimisticResponse: { - __typename: 'Mutation', - setAssetTags: { - ...asset, - tags, - }, - }, - // The ASSETS query should be triggered to again show the full amount of assets in the current collection - // FIXME: The TAGS query is triggered to update the asset count in the asset collection list, which could be modified directly in the cache update method below refetchQueries: ['ASSETS', 'TAGS'], + }).then(({ data: { setAssetTags: result } }) => { + if (!result.success) { + throw new Error(result.messages.join(', ')); + } }); return { setAssetTags, data, error, loading }; diff --git a/packages/core/src/hooks/useUpdateAsset.ts b/packages/core/src/hooks/useUpdateAsset.ts index 64212ed04..d27aae157 100644 --- a/packages/core/src/hooks/useUpdateAsset.ts +++ b/packages/core/src/hooks/useUpdateAsset.ts @@ -18,10 +18,9 @@ interface UpdateAssetVariables { } export default function useUpdateAsset() { - const [action, { error, data, loading }] = useMutation< - { __typename: string; updateAsset: Asset }, - UpdateAssetVariables - >(UPDATE_ASSET); + const [action, { error, data, loading }] = useMutation<{ updateAsset: MutationResult }, UpdateAssetVariables>( + UPDATE_ASSET + ); const updateAsset = ({ asset, label, caption, copyrightNotice }: UpdateAssetProps) => action({ @@ -32,15 +31,11 @@ export default function useUpdateAsset() { caption, copyrightNotice, }, - optimisticResponse: { - __typename: 'Mutation', - updateAsset: { - ...asset, - label, - caption, - copyrightNotice, - }, - }, + refetchQueries: ['ASSETS'], + }).then(({ data: { updateAsset: result } }) => { + if (!result.success) { + throw new Error(result.messages.join(', ')); + } }); return { updateAsset, data, error, loading }; diff --git a/packages/core/src/hooks/useUpdateAssets.ts b/packages/core/src/hooks/useUpdateAssets.ts new file mode 100644 index 000000000..caa07f217 --- /dev/null +++ b/packages/core/src/hooks/useUpdateAssets.ts @@ -0,0 +1,25 @@ +import { useMutation } from '@apollo/client'; + +import { UPDATE_ASSETS } from '../mutations'; + +interface UpdateAssetsVariables { + identities: { assetId: string; assetSourceId: string }[]; + copyrightNotice?: string; +} + +export default function useUpdateAssets() { + const [action, { error, data, loading }] = useMutation<{ updateAssets: MutationResult[] }, UpdateAssetsVariables>( + UPDATE_ASSETS + ); + + const updateAssets = (identities: AssetIdentity[], copyrightNotice: string) => + action({ + variables: { + identities: identities.map(({ assetId, assetSourceId }) => ({ assetId, assetSourceId })), + copyrightNotice, + }, + refetchQueries: ['ASSETS'], + }).then(({ data }) => data.updateAssets); + + return { updateAssets, data, error, loading }; +} diff --git a/packages/core/src/mutations/assignAssetsToCollection.ts b/packages/core/src/mutations/assignAssetsToCollection.ts new file mode 100644 index 000000000..5e99e1a2c --- /dev/null +++ b/packages/core/src/mutations/assignAssetsToCollection.ts @@ -0,0 +1,12 @@ +import { gql } from '@apollo/client'; + +const ASSIGN_ASSETS_TO_COLLECTION = gql` + mutation AssignAssetsToCollection($identities: [AssetIdentityInput!]!, $assetCollectionId: AssetCollectionId!) { + assignAssetsToCollection(identities: $identities, assetCollectionId: $assetCollectionId) { + success + messages + } + } +`; + +export default ASSIGN_ASSETS_TO_COLLECTION; diff --git a/packages/core/src/mutations/deleteAssets.ts b/packages/core/src/mutations/deleteAssets.ts new file mode 100644 index 000000000..e48b527c0 --- /dev/null +++ b/packages/core/src/mutations/deleteAssets.ts @@ -0,0 +1,12 @@ +import { gql } from '@apollo/client'; + +const DELETE_ASSETS = gql` + mutation DeleteAssets($identities: [AssetIdentityInput!]!) { + deleteAssets(identities: $identities) { + success + messages + } + } +`; + +export default DELETE_ASSETS; diff --git a/packages/core/src/mutations/index.ts b/packages/core/src/mutations/index.ts index fdd9189bb..dcc2fd53d 100644 --- a/packages/core/src/mutations/index.ts +++ b/packages/core/src/mutations/index.ts @@ -1,7 +1,12 @@ +export { default as ASSIGN_ASSETS_TO_COLLECTION } from './assignAssetsToCollection'; export { default as DELETE_ASSET } from './deleteAsset'; +export { default as DELETE_ASSETS } from './deleteAssets'; export { default as IMPORT_ASSET } from './importAsset'; export { default as SET_ASSET_COLLECTIONS } from './setAssetCollections'; export { default as SET_ASSET_TAGS } from './setAssetTags'; export { default as TAG_ASSET } from './tagAsset'; +export { default as TAG_ASSETS } from './tagAssets'; export { default as UNTAG_ASSET } from './untagAsset'; +export { default as UNTAG_ASSETS } from './untagAssets'; export { default as UPDATE_ASSET } from './updateAsset'; +export { default as UPDATE_ASSETS } from './updateAssets'; diff --git a/packages/core/src/mutations/setAssetTags.ts b/packages/core/src/mutations/setAssetTags.ts index 364bf42f2..d1dba5695 100644 --- a/packages/core/src/mutations/setAssetTags.ts +++ b/packages/core/src/mutations/setAssetTags.ts @@ -1,20 +1,12 @@ import { gql } from '@apollo/client'; -import { ASSET_FRAGMENT } from '../fragments/asset'; - const SET_ASSET_TAGS = gql` - mutation SetAssetTags( - $id: AssetId! - $assetSourceId: AssetSourceId! - $tagIds: [TagId!]! - $includeUsage: Boolean = false - ) { - includeUsage @client(always: true) @export(as: "includeUsage") + mutation SetAssetTags($id: AssetId!, $assetSourceId: AssetSourceId!, $tagIds: [TagId!]!) { setAssetTags(id: $id, assetSourceId: $assetSourceId, tagIds: $tagIds) { - ...AssetProps + success + messages } } - ${ASSET_FRAGMENT} `; export default SET_ASSET_TAGS; diff --git a/packages/core/src/mutations/tagAsset.ts b/packages/core/src/mutations/tagAsset.ts index 88e21df01..d93cde6ed 100644 --- a/packages/core/src/mutations/tagAsset.ts +++ b/packages/core/src/mutations/tagAsset.ts @@ -1,15 +1,12 @@ import { gql } from '@apollo/client'; -import { ASSET_FRAGMENT } from '../fragments/asset'; - const TAG_ASSET = gql` - mutation TagAsset($id: AssetId!, $assetSourceId: AssetSourceId!, $tagId: TagId!, $includeUsage: Boolean = false) { - includeUsage @client(always: true) @export(as: "includeUsage") + mutation TagAsset($id: AssetId!, $assetSourceId: AssetSourceId!, $tagId: TagId!) { tagAsset(id: $id, assetSourceId: $assetSourceId, tagId: $tagId) { - ...AssetProps + success + messages } } - ${ASSET_FRAGMENT} `; export default TAG_ASSET; diff --git a/packages/core/src/mutations/tagAssets.ts b/packages/core/src/mutations/tagAssets.ts new file mode 100644 index 000000000..8827bb8db --- /dev/null +++ b/packages/core/src/mutations/tagAssets.ts @@ -0,0 +1,12 @@ +import { gql } from '@apollo/client'; + +const TAG_ASSETS = gql` + mutation TagAssets($identities: [AssetIdentityInput!]!, $tagId: TagId!) { + tagAssets(identities: $identities, tagId: $tagId) { + success + messages + } + } +`; + +export default TAG_ASSETS; diff --git a/packages/core/src/mutations/untagAsset.ts b/packages/core/src/mutations/untagAsset.ts index 764c16535..2dce114f6 100644 --- a/packages/core/src/mutations/untagAsset.ts +++ b/packages/core/src/mutations/untagAsset.ts @@ -1,15 +1,12 @@ import { gql } from '@apollo/client'; -import { ASSET_FRAGMENT } from '../fragments/asset'; - const UNTAG_ASSET = gql` - mutation UntagAsset($id: AssetId!, $assetSourceId: AssetSourceId!, $tagId: TagId!, $includeUsage: Boolean = false) { - includeUsage @client(always: true) @export(as: "includeUsage") + mutation UntagAsset($id: AssetId!, $assetSourceId: AssetSourceId!, $tagId: TagId!) { untagAsset(id: $id, assetSourceId: $assetSourceId, tagId: $tagId) { - ...AssetProps + success + messages } } - ${ASSET_FRAGMENT} `; export default UNTAG_ASSET; diff --git a/packages/core/src/mutations/untagAssets.ts b/packages/core/src/mutations/untagAssets.ts new file mode 100644 index 000000000..4c115eafc --- /dev/null +++ b/packages/core/src/mutations/untagAssets.ts @@ -0,0 +1,12 @@ +import { gql } from '@apollo/client'; + +const UNTAG_ASSETS = gql` + mutation UntagAssets($identities: [AssetIdentityInput!]!, $tagId: TagId!) { + untagAssets(identities: $identities, tagId: $tagId) { + success + messages + } + } +`; + +export default UNTAG_ASSETS; diff --git a/packages/core/src/mutations/updateAsset.ts b/packages/core/src/mutations/updateAsset.ts index c1bd4337e..4d751570d 100644 --- a/packages/core/src/mutations/updateAsset.ts +++ b/packages/core/src/mutations/updateAsset.ts @@ -1,7 +1,5 @@ import { gql } from '@apollo/client'; -import { ASSET_FRAGMENT } from '../fragments/asset'; - const UPDATE_ASSET = gql` mutation UpdateAsset( $id: AssetId! @@ -9,9 +7,7 @@ const UPDATE_ASSET = gql` $label: String $caption: String $copyrightNotice: String - $includeUsage: Boolean = false ) { - includeUsage @client(always: true) @export(as: "includeUsage") updateAsset( id: $id assetSourceId: $assetSourceId @@ -19,10 +15,10 @@ const UPDATE_ASSET = gql` caption: $caption copyrightNotice: $copyrightNotice ) { - ...AssetProps + success + messages } } - ${ASSET_FRAGMENT} `; export default UPDATE_ASSET; diff --git a/packages/core/src/mutations/updateAssets.ts b/packages/core/src/mutations/updateAssets.ts new file mode 100644 index 000000000..d8893cee7 --- /dev/null +++ b/packages/core/src/mutations/updateAssets.ts @@ -0,0 +1,12 @@ +import { gql } from '@apollo/client'; + +const UPDATE_ASSETS = gql` + mutation UpdateAssets($identities: [AssetIdentityInput!]!, $copyrightNotice: String) { + updateAssets(identities: $identities, copyrightNotice: $copyrightNotice) { + success + messages + } + } +`; + +export default UPDATE_ASSETS; diff --git a/packages/dev-server/src/server.ts b/packages/dev-server/src/server.ts index f8f24cca7..6e482b224 100644 --- a/packages/dev-server/src/server.ts +++ b/packages/dev-server/src/server.ts @@ -165,18 +165,19 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)); }), }, Mutation: { - updateAsset: ($_, { id, assetSourceId, label, caption, copyrightNotice }): Asset => { + updateAsset: ($_, { id, assetSourceId, label, caption, copyrightNotice }) => { const asset = assets.find((asset) => asset.id === id && asset.assetSource.id === assetSourceId); - asset.label = label; - asset.caption = caption; - asset.copyrightNotice = copyrightNotice; + if (!asset) return { success: false, messages: ['Asset not found'] }; + if (label !== undefined) asset.label = label; + if (caption !== undefined) asset.caption = caption; + if (copyrightNotice !== undefined) asset.copyrightNotice = copyrightNotice; asset.lastModified = new Date(); addAssetChange({ lastModified: asset.lastModified, assetId: id, type: 'ASSET_UPDATED', }); - return asset; + return { success: true, messages: [] }; }, setAssetCollectionParent: ( $_, @@ -251,15 +252,16 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)); setAssetTags: ( $_, { id, assetSourceId, tagIds }: { id: string; assetSourceId: string; tagIds: string[] } - ): Asset => { + ) => { const asset = assets.find((asset) => asset.id === id && asset.assetSource.id === assetSourceId); + if (!asset) return { success: false, messages: ['Asset not found'] }; asset.tags = tags.filter((tag) => tagIds.includes(tag.id)); addAssetChange({ lastModified: asset.lastModified, assetId: id, type: 'ASSET_UPDATED', }); - return asset; + return { success: true, messages: [] }; }, setAssetCollections: ( $_, @@ -329,11 +331,84 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)); editAsset: ($_, { id, assetSourceId, filename, options }): boolean => { throw new Error('Not implemented'); }, - tagAsset: ($_, { id, assetSourceId, tagId }): Asset => { - throw new Error('Not implemented'); + tagAsset: ($_, { id, assetSourceId, tagId }) => { + const asset = assets.find((asset) => asset.id === id && asset.assetSource.id === assetSourceId); + if (!asset) return { success: false, messages: ['Asset not found'] }; + const tag = tags.find((tag) => tag.id === tagId); + if (!tag) return { success: false, messages: ['Tag not found'] }; + if (!asset.tags.find((t) => t.id === tagId)) { + asset.tags.push(tag); + } + addAssetChange({ lastModified: new Date(), assetId: id, type: 'ASSET_UPDATED' }); + return { success: true, messages: [] }; }, - untagAsset: ($_, { id, assetSourceId, tagId }): Asset => { - throw new Error('Not implemented'); + untagAsset: ($_, { id, assetSourceId, tagId }) => { + const asset = assets.find((asset) => asset.id === id && asset.assetSource.id === assetSourceId); + if (!asset) return { success: false, messages: ['Asset not found'] }; + asset.tags = asset.tags.filter((tag) => tag.id !== tagId); + addAssetChange({ lastModified: new Date(), assetId: id, type: 'ASSET_UPDATED' }); + return { success: true, messages: [] }; + }, + deleteAssets: ($_, { identities }) => { + return identities.map(({ assetId, assetSourceId }) => { + const inUse = Fixtures.getUsageDetailsForAsset(assetId).reduce( + (prev, { usages }) => prev || usages.length > 0, + false + ); + if (inUse) { + return { success: false, messages: ['Asset is in use'] }; + } + const assetIndex = assets.findIndex( + (asset) => asset.id === assetId && asset.assetSource.id === assetSourceId + ); + if (assetIndex >= 0) { + assets.splice(assetIndex, 1); + addAssetChange({ lastModified: new Date(), assetId, type: 'ASSET_REMOVED' }); + return { success: true, messages: [] }; + } + return { success: false, messages: ['Asset not found'] }; + }); + }, + tagAssets: ($_, { identities, tagId }) => { + const tag = tags.find((tag) => tag.id === tagId); + if (!tag) return [{ success: false, messages: ['Tag not found'] }]; + return identities.map(({ assetId, assetSourceId }) => { + const asset = assets.find((a) => a.id === assetId && a.assetSource.id === assetSourceId); + if (!asset) return { success: false, messages: ['Asset not found'] }; + if (!asset.tags.find((t) => t.id === tagId)) { + asset.tags.push(tag); + } + return { success: true, messages: [] }; + }); + }, + untagAssets: ($_, { identities, tagId }) => { + const tag = tags.find((tag) => tag.id === tagId); + if (!tag) return [{ success: false, messages: ['Tag not found'] }]; + return identities.map(({ assetId, assetSourceId }) => { + const asset = assets.find((a) => a.id === assetId && a.assetSource.id === assetSourceId); + if (!asset) return { success: false, messages: ['Asset not found'] }; + asset.tags = asset.tags.filter((t) => t.id !== tagId); + return { success: true, messages: [] }; + }); + }, + assignAssetsToCollection: ($_, { identities, assetCollectionId }) => { + const collection = assetCollections.find((c) => c.id === assetCollectionId); + if (!collection) return [{ success: false, messages: ['Collection not found'] }]; + return identities.map(({ assetId, assetSourceId }) => { + const asset = assets.find((a) => a.id === assetId && a.assetSource.id === assetSourceId); + if (!asset) return { success: false, messages: ['Asset not found'] }; + asset.collections = [collection]; + return { success: true, messages: [] }; + }); + }, + updateAssets: ($_, { identities, copyrightNotice }) => { + return identities.map(({ assetId, assetSourceId }) => { + const asset = assets.find((a) => a.id === assetId && a.assetSource.id === assetSourceId); + if (!asset) return { success: false, messages: ['Asset not found'] }; + if (copyrightNotice !== undefined) asset.copyrightNotice = copyrightNotice; + asset.lastModified = new Date(); + return { success: true, messages: [] }; + }); }, uploadFiles: ($_, { files, tagId, assetCollectionId }): FileUploadResult[] => { throw new Error('Not implemented'); diff --git a/packages/media-module/src/components/Actions/DeleteAssetButton.tsx b/packages/media-module/src/components/Actions/DeleteAssetButton.tsx index a02d14cd2..8068af745 100644 --- a/packages/media-module/src/components/Actions/DeleteAssetButton.tsx +++ b/packages/media-module/src/components/Actions/DeleteAssetButton.tsx @@ -3,7 +3,7 @@ import React, { useCallback } from 'react'; import { Icon, IconButton } from '@neos-project/react-ui-components'; import { useIntl, useMediaUi, useNotify } from '@media-ui/core'; -import { useDeleteAsset } from '@media-ui/core/src/hooks'; +import { useDeleteAsset, useDeleteAssets } from '@media-ui/core/src/hooks'; import { useFailedAssetLabels } from '@media-ui/media-module/src/hooks'; interface DeleteAssetButtonProps { @@ -28,7 +28,8 @@ const DeleteAssetButton: React.FC = ({ const { translate } = useIntl(); const { approvalAttainmentStrategy } = useMediaUi(); const { deleteAsset } = useDeleteAsset(); - const { getFailedAssetLabels } = useFailedAssetLabels(); + const { deleteAssets } = useDeleteAssets(); + const { getFailedAssetLabelsFromResults } = useFailedAssetLabels(); const Notify = useNotify(); const isSingle = !assets && !!asset; @@ -55,8 +56,8 @@ const DeleteAssetButton: React.FC = ({ } // Multi-asset process - const results = await Promise.allSettled(identities.map((identity) => deleteAsset(identity))); - const failedLabels = getFailedAssetLabels(results, identities); + const results = await deleteAssets(identities); + const failedLabels = getFailedAssetLabelsFromResults(results, identities); if (failedLabels.length === 0) { Notify.ok(translate('action.deleteAssets.success', 'The assets have been deleted')); @@ -68,7 +69,17 @@ const DeleteAssetButton: React.FC = ({ failedLabels.join(', ') ); return false; - }, [asset, assets, isSingle, Notify, translate, deleteAsset, approvalAttainmentStrategy, getFailedAssetLabels]); + }, [ + asset, + assets, + isSingle, + Notify, + translate, + deleteAsset, + deleteAssets, + approvalAttainmentStrategy, + getFailedAssetLabelsFromResults, + ]); if (isSingle && asset.assetSource.readOnly) return null; diff --git a/packages/media-module/src/components/SideBarRight/Inspector/CollectionSelectBox.tsx b/packages/media-module/src/components/SideBarRight/Inspector/CollectionSelectBox.tsx index da016ee1d..bc8831a58 100644 --- a/packages/media-module/src/components/SideBarRight/Inspector/CollectionSelectBox.tsx +++ b/packages/media-module/src/components/SideBarRight/Inspector/CollectionSelectBox.tsx @@ -4,7 +4,12 @@ import { useRecoilValue } from 'recoil'; import { Headline, MultiSelectBox, SelectBox } from '@neos-project/react-ui-components'; import { useIntl, useNotify, useMediaUi } from '@media-ui/core'; -import { useConfigQuery, useSelectedAsset, useSetAssetCollections } from '@media-ui/core/src/hooks'; +import { + useAssignAssetsToCollection, + useConfigQuery, + useSelectedAsset, + useSetAssetCollections, +} from '@media-ui/core/src/hooks'; import { useFailedAssetLabels } from '@media-ui/media-module/src/hooks'; import { IconLabel } from '@media-ui/core/src/components'; import { featureFlagsState, selectedAssetIdsState } from '@media-ui/core/src/state'; @@ -31,7 +36,8 @@ const CollectionSelectBox: React.FC = () => { const selectedAssetSourceId = useRecoilValue(selectedAssetSourceState); const { assetCollections } = useAssetCollectionsQuery(selectedAssetSourceId); const { setAssetCollections, loading } = useSetAssetCollections(); - const { getFailedAssetLabels } = useFailedAssetLabels(); + const { assignAssetsToCollection } = useAssignAssetsToCollection(); + const { getFailedAssetLabelsFromResults } = useFailedAssetLabels(); const selectedAsset = useSelectedAsset(); const { limitToSingleAssetCollectionPerAsset } = useRecoilValue(featureFlagsState); const [searchTerm, setSearchTerm] = useState(''); @@ -84,15 +90,8 @@ const CollectionSelectBox: React.FC = () => { }); if (!canSetCollection) return; - const results = await Promise.allSettled( - selectedAssets.map((identity) => - setAssetCollections({ - asset: { id: identity.assetId, assetSource: { id: identity.assetSourceId } } as Asset, - assetCollections: newAssetCollections, - }) - ) - ); - const failedLabels = getFailedAssetLabels(results, selectedAssets); + const results = await assignAssetsToCollection(selectedAssets, targetCollection.id); + const failedLabels = getFailedAssetLabelsFromResults(results, selectedAssets); if (failedLabels.length === 0) { Notify.ok( @@ -153,12 +152,13 @@ const CollectionSelectBox: React.FC = () => { selectedAssets, selectedAsset, setAssetCollections, + assignAssetsToCollection, assetCollections, translate, syncSelectedAssetCollectionIds, obtainApprovalToSetAssetCollections, obtainApprovalToShiftAssetsToCollection, - getFailedAssetLabels, + getFailedAssetLabelsFromResults, ] ); diff --git a/packages/media-module/src/components/SideBarRight/Inspector/PropertyInspector.tsx b/packages/media-module/src/components/SideBarRight/Inspector/PropertyInspector.tsx index e4967039f..e7c40673e 100644 --- a/packages/media-module/src/components/SideBarRight/Inspector/PropertyInspector.tsx +++ b/packages/media-module/src/components/SideBarRight/Inspector/PropertyInspector.tsx @@ -1,15 +1,13 @@ import React, { useCallback, useEffect, useState } from 'react'; import { useRecoilValue } from 'recoil'; -import { useApolloClient } from '@apollo/client'; import { TextArea, TextInput, ToggablePanel } from '@neos-project/react-ui-components'; import { useIntl, useNotify, useMediaUi } from '@media-ui/core'; -import { useSelectedAsset, useUpdateAsset } from '@media-ui/core/src/hooks'; +import { useSelectedAsset, useUpdateAsset, useUpdateAssets } from '@media-ui/core/src/hooks'; import { useFailedAssetLabels } from '@media-ui/media-module/src/hooks'; import { IconLabel } from '@media-ui/core/src/components'; import { featureFlagsState, selectedAssetIdsState } from '@media-ui/core/src/state'; -import { UPDATE_ASSET } from '@media-ui/core/src/mutations'; import { useInteraction } from '@media-ui/core/src/provider'; import { CollectionSelectBox, MetadataView, TagSelectBoxAsset } from './index'; @@ -33,8 +31,8 @@ const PropertyInspector = () => { approvalAttainmentStrategy: { obtainApprovalToUpdateAsset }, } = useMediaUi(); const { confirm } = useInteraction(); - const client = useApolloClient(); - const { getFailedAssetLabels } = useFailedAssetLabels(); + const { updateAssets } = useUpdateAssets(); + const { getFailedAssetLabelsFromResults } = useFailedAssetLabels(); const featureFlags = useRecoilValue(featureFlagsState); const [label, setLabel] = useState(null); const [caption, setCaption] = useState(null); @@ -116,21 +114,8 @@ const PropertyInspector = () => { setMultiLoading(true); - const mutations = selectedAssets.map((identity) => - client.mutate({ - mutation: UPDATE_ASSET, - variables: { - id: identity.assetId, - assetSourceId: identity.assetSourceId, - copyrightNotice, - }, - }) - ); - - const results = await Promise.allSettled(mutations); - const failedLabels = getFailedAssetLabels(results, selectedAssets); - - await client.reFetchObservableQueries(); + const results = await updateAssets(selectedAssets, copyrightNotice); + const failedLabels = getFailedAssetLabelsFromResults(results, selectedAssets); if (failedLabels.length === 0) { Notify.ok( @@ -145,7 +130,7 @@ const PropertyInspector = () => { } setMultiLoading(false); - }, [selectedAssets, copyrightNotice, confirm, translate, client, getFailedAssetLabels, Notify]); + }, [selectedAssets, copyrightNotice, confirm, translate, updateAssets, getFailedAssetLabelsFromResults, Notify]); useEffect(() => { handleDiscard(); diff --git a/packages/media-module/src/components/SideBarRight/Inspector/TagSelectBoxMulti.tsx b/packages/media-module/src/components/SideBarRight/Inspector/TagSelectBoxMulti.tsx index 3e0001206..9bfe8de3f 100644 --- a/packages/media-module/src/components/SideBarRight/Inspector/TagSelectBoxMulti.tsx +++ b/packages/media-module/src/components/SideBarRight/Inspector/TagSelectBoxMulti.tsx @@ -1,11 +1,10 @@ import React, { useCallback, useMemo, useState } from 'react'; import { useRecoilValue } from 'recoil'; -import { useApolloClient } from '@apollo/client'; import { Headline, SelectBox } from '@neos-project/react-ui-components'; import { useIntl, useNotify, useMediaUi } from '@media-ui/core'; -import { useFailedAssetLabels, useTagAsset, useUntagAssetById } from '@media-ui/media-module/src/hooks'; +import { useFailedAssetLabels, useTagAssets, useUntagAssets } from '@media-ui/media-module/src/hooks'; import { IconLabel } from '@media-ui/core/src/components'; import { selectedAssetIdsState } from '@media-ui/core/src/state'; import { useTagsQuery } from '@media-ui/feature-asset-tags'; @@ -16,16 +15,15 @@ import { selectedAssetSourceState } from '@media-ui/feature-asset-sources'; const TagSelectBoxMulti: React.FC = () => { const { translate } = useIntl(); const Notify = useNotify(); - const client = useApolloClient(); - const { getFailedAssetLabels } = useFailedAssetLabels(); + const { getFailedAssetLabelsFromResults } = useFailedAssetLabels(); const { approvalAttainmentStrategy: { obtainApprovalToTagAssets, obtainApprovalToUntagAssets }, } = useMediaUi(); const selectedAssets = useRecoilValue(selectedAssetIdsState); const selectedAssetSourceId = useRecoilValue(selectedAssetSourceState); const { tags } = useTagsQuery(selectedAssetSourceId); - const { tagAsset } = useTagAsset(); - const { untagAssetById } = useUntagAssetById(); + const { tagAssets } = useTagAssets(); + const { untagAssets } = useUntagAssets(); const [addSearchTerm, setAddSearchTerm] = useState(''); const [removeSearchTerm, setRemoveSearchTerm] = useState(''); @@ -51,17 +49,8 @@ const TagSelectBoxMulti: React.FC = () => { const canTag = await obtainApprovalToTagAssets({ assets: selectedAssets, tag }); if (!canTag) return; - const mutations = selectedAssets.map((identity) => - tagAsset({ - asset: { id: identity.assetId, assetSource: { id: identity.assetSourceId } }, - tagId, - }) - ); - - const results = await Promise.allSettled(mutations); - const failedLabels = getFailedAssetLabels(results, selectedAssets); - - await client.reFetchObservableQueries(); + const results = await tagAssets(selectedAssets, tagId); + const failedLabels = getFailedAssetLabelsFromResults(results, selectedAssets); if (failedLabels.length === 0) { Notify.ok(translate('actions.bulkTagAssets.success', 'The tag has been added to the selected assets')); @@ -72,7 +61,7 @@ const TagSelectBoxMulti: React.FC = () => { ); } }, - [selectedAssets, tags, tagAsset, client, Notify, translate, obtainApprovalToTagAssets, getFailedAssetLabels] + [selectedAssets, tags, tagAssets, Notify, translate, obtainApprovalToTagAssets, getFailedAssetLabelsFromResults] ); const handleRemove = useCallback( @@ -85,17 +74,8 @@ const TagSelectBoxMulti: React.FC = () => { const canUntag = await obtainApprovalToUntagAssets({ assets: selectedAssets, tag }); if (!canUntag) return; - const mutations = selectedAssets.map((identity) => - untagAssetById({ - asset: { id: identity.assetId, assetSource: { id: identity.assetSourceId } }, - tagId, - }) - ); - - const results = await Promise.allSettled(mutations); - const failedLabels = getFailedAssetLabels(results, selectedAssets); - - await client.reFetchObservableQueries(); + const results = await untagAssets(selectedAssets, tagId); + const failedLabels = getFailedAssetLabelsFromResults(results, selectedAssets); if (failedLabels.length === 0) { Notify.ok( @@ -111,12 +91,11 @@ const TagSelectBoxMulti: React.FC = () => { [ selectedAssets, tags, - untagAssetById, - client, + untagAssets, Notify, translate, obtainApprovalToUntagAssets, - getFailedAssetLabels, + getFailedAssetLabelsFromResults, ] ); diff --git a/packages/media-module/src/hooks/index.ts b/packages/media-module/src/hooks/index.ts index 94d23116b..c64ee2ca0 100644 --- a/packages/media-module/src/hooks/index.ts +++ b/packages/media-module/src/hooks/index.ts @@ -3,4 +3,6 @@ export { default as useAssetSelection } from './useAssetSelection'; export { default as useFailedAssetLabels } from './useFailedAssetLabels'; export { default as useSelectAssets } from './useSelectAssets'; export { default as useTagAsset } from './useTagAsset'; +export { default as useTagAssets } from './useTagAssets'; export { default as useUntagAssetById } from './useUntagAssetById'; +export { default as useUntagAssets } from './useUntagAssets'; diff --git a/packages/media-module/src/hooks/useFailedAssetLabels.ts b/packages/media-module/src/hooks/useFailedAssetLabels.ts index df7db36fd..f4015e62c 100644 --- a/packages/media-module/src/hooks/useFailedAssetLabels.ts +++ b/packages/media-module/src/hooks/useFailedAssetLabels.ts @@ -31,5 +31,13 @@ export default function useFailedAssetLabels() { [getAssetLabel] ); - return { getAssetLabel, getFailedAssetLabels }; + const getFailedAssetLabelsFromResults = useCallback( + (results: MutationResult[], identities: AssetIdentity[]): string[] => + results + .map((result, index) => (!result.success ? getAssetLabel(identities[index].assetId) : null)) + .filter(Boolean), + [getAssetLabel] + ); + + return { getAssetLabel, getFailedAssetLabels, getFailedAssetLabelsFromResults }; } diff --git a/packages/media-module/src/hooks/useTagAsset.ts b/packages/media-module/src/hooks/useTagAsset.ts index 038ba4f7e..9f97bc597 100644 --- a/packages/media-module/src/hooks/useTagAsset.ts +++ b/packages/media-module/src/hooks/useTagAsset.ts @@ -14,9 +14,7 @@ interface TagAssetVariables { } export default function useTagAsset() { - const [action, { error, data, loading }] = useMutation<{ __typename: string; tagAsset: Asset }, TagAssetVariables>( - TAG_ASSET - ); + const [action, { error, data, loading }] = useMutation<{ tagAsset: MutationResult }, TagAssetVariables>(TAG_ASSET); const tagAsset = ({ asset, tagId }: TagAssetProps) => action({ @@ -25,6 +23,7 @@ export default function useTagAsset() { assetSourceId: asset.assetSource.id, tagId, }, + refetchQueries: ['ASSETS'], }); return { tagAsset, data, error, loading }; diff --git a/packages/media-module/src/hooks/useTagAssets.ts b/packages/media-module/src/hooks/useTagAssets.ts new file mode 100644 index 000000000..790e11c70 --- /dev/null +++ b/packages/media-module/src/hooks/useTagAssets.ts @@ -0,0 +1,25 @@ +import { useMutation } from '@apollo/client'; + +import { TAG_ASSETS } from '@media-ui/core/src/mutations'; + +interface TagAssetsVariables { + identities: { assetId: string; assetSourceId: string }[]; + tagId: string; +} + +export default function useTagAssets() { + const [action, { error, data, loading }] = useMutation<{ tagAssets: MutationResult[] }, TagAssetsVariables>( + TAG_ASSETS + ); + + const tagAssets = (identities: AssetIdentity[], tagId: string) => + action({ + variables: { + identities: identities.map(({ assetId, assetSourceId }) => ({ assetId, assetSourceId })), + tagId, + }, + refetchQueries: ['ASSETS'], + }).then(({ data }) => data.tagAssets); + + return { tagAssets, data, error, loading }; +} diff --git a/packages/media-module/src/hooks/useUntagAssetById.ts b/packages/media-module/src/hooks/useUntagAssetById.ts index 822fcec65..6ba7b2580 100644 --- a/packages/media-module/src/hooks/useUntagAssetById.ts +++ b/packages/media-module/src/hooks/useUntagAssetById.ts @@ -14,10 +14,9 @@ interface UntagAssetByIdVariables { } export default function useUntagAssetById() { - const [action, { error, data, loading }] = useMutation< - { __typename: string; untagAsset: Asset }, - UntagAssetByIdVariables - >(UNTAG_ASSET); + const [action, { error, data, loading }] = useMutation<{ untagAsset: MutationResult }, UntagAssetByIdVariables>( + UNTAG_ASSET + ); const untagAssetById = ({ asset, tagId }: UntagAssetByIdProps) => action({ @@ -26,6 +25,7 @@ export default function useUntagAssetById() { assetSourceId: asset.assetSource.id, tagId, }, + refetchQueries: ['ASSETS'], }); return { untagAssetById, data, error, loading }; diff --git a/packages/media-module/src/hooks/useUntagAssets.ts b/packages/media-module/src/hooks/useUntagAssets.ts new file mode 100644 index 000000000..5087f224e --- /dev/null +++ b/packages/media-module/src/hooks/useUntagAssets.ts @@ -0,0 +1,25 @@ +import { useMutation } from '@apollo/client'; + +import { UNTAG_ASSETS } from '@media-ui/core/src/mutations'; + +interface UntagAssetsVariables { + identities: { assetId: string; assetSourceId: string }[]; + tagId: string; +} + +export default function useUntagAssets() { + const [action, { error, data, loading }] = useMutation<{ untagAssets: MutationResult[] }, UntagAssetsVariables>( + UNTAG_ASSETS + ); + + const untagAssets = (identities: AssetIdentity[], tagId: string) => + action({ + variables: { + identities: identities.map(({ assetId, assetSourceId }) => ({ assetId, assetSourceId })), + tagId, + }, + refetchQueries: ['ASSETS'], + }).then(({ data }) => data.untagAssets); + + return { untagAssets, data, error, loading }; +}