diff --git a/Resources/Private/JavaScript/asset-collections/src/components/AddTagButton.tsx b/Resources/Private/JavaScript/asset-collections/src/components/AddTagButton.tsx index 48e9c3a36..65b3f82c2 100644 --- a/Resources/Private/JavaScript/asset-collections/src/components/AddTagButton.tsx +++ b/Resources/Private/JavaScript/asset-collections/src/components/AddTagButton.tsx @@ -16,7 +16,15 @@ const AddTagButton: React.FC = () => { const selectedTagId = useRecoilValue(selectedTagIdState); const onClickCreate = useCallback(() => { - setCreateTagDialogState({ label: '', visible: true }); + setCreateTagDialogState({ + visible: true, + label: '', + tags: [], + validation: { + valid: false, + errors: [], + }, + }); }, [setCreateTagDialogState]); return ( diff --git a/Resources/Private/JavaScript/asset-tags/src/components/CreateTagDialog.tsx b/Resources/Private/JavaScript/asset-tags/src/components/CreateTagDialog.tsx index f2af832f0..3f22e7148 100644 --- a/Resources/Private/JavaScript/asset-tags/src/components/CreateTagDialog.tsx +++ b/Resources/Private/JavaScript/asset-tags/src/components/CreateTagDialog.tsx @@ -2,11 +2,13 @@ import * as React from 'react'; import { useCallback } from 'react'; import { useRecoilState } from 'recoil'; -import { Button, Label, TextInput } from '@neos-project/react-ui-components'; +import { Button, Label, TextInput, Tooltip } from '@neos-project/react-ui-components'; +import TAGS from '../queries/tags'; +import { useQuery } from '@apollo/client'; import { useIntl, useNotify } from '@media-ui/core'; import { useSelectedAssetCollection } from '@media-ui/feature-asset-collections'; -import { useCreateTag } from '@media-ui/feature-asset-tags'; +import { useCreateTag, useTagsQuery } from '@media-ui/feature-asset-tags'; import { Dialog } from '@media-ui/core/src/components'; import createTagDialogState from '../state/createTagDialogState'; @@ -18,10 +20,22 @@ const CreateTagDialog: React.FC = () => { const Notify = useNotify(); const selectedAssetCollection = useSelectedAssetCollection(); const [dialogState, setDialogState] = useRecoilState(createTagDialogState); - const createPossible = !!(dialogState.label && dialogState.label.trim()); const { createTag } = useCreateTag(); + const { tags } = useTagsQuery(); - const handleRequestClose = useCallback(() => setDialogState({ visible: false, label: '' }), [setDialogState]); + const handleRequestClose = useCallback( + () => + setDialogState({ + visible: false, + label: '', + tags: [], + validation: { + valid: false, + errors: [], + }, + }), + [setDialogState] + ); const handleCreate = useCallback(() => { setDialogState((state) => ({ ...state, visible: false })); createTag(dialogState.label, selectedAssetCollection?.id) @@ -32,7 +46,32 @@ const CreateTagDialog: React.FC = () => { return; }); }, [Notify, setDialogState, createTag, dialogState, translate, selectedAssetCollection]); - const setLabel = useCallback((label) => setDialogState((state) => ({ ...state, label })), [setDialogState]); + const validate = (label) => { + const validationErrors = []; + const trimmedLabel = label.trim(); + const tagWithLabelExist = tags?.some((tag) => tag.label === trimmedLabel); + + if (trimmedLabel.length === 0) { + validationErrors.push(translate('tagActions.validation.emtpyTagLabl', 'Please provide a tag label')); + } + + if (tagWithLabelExist) { + validationErrors.push(translate('tagActions.validation.tagExists', 'A tag with this label already exists')); + } + + const validation = { + errors: validationErrors, + valid: validationErrors.length === 0, + }; + setDialogState((state) => ({ ...state, validation })); + }; + const setLabel = useCallback( + (label) => { + validate(label); + setDialogState((state) => ({ ...state, label })); + }, + [setDialogState] + ); return ( { key="upload" style="success" hoverStyle="success" - disabled={!createPossible} + disabled={!dialogState.validation?.valid} onClick={handleCreate} > {translate('general.create', 'Create')} @@ -58,11 +97,22 @@ const CreateTagDialog: React.FC = () => { + {dialogState.validation?.errors?.length > 0 && ( + +
    + {dialogState.validation.errors.map((error, index) => ( +
  • {error}
  • + ))} +
+
+ )}
); diff --git a/Resources/Private/JavaScript/asset-tags/src/state/createTagDialogState.ts b/Resources/Private/JavaScript/asset-tags/src/state/createTagDialogState.ts index 5e9e1d1ce..b0b1b4691 100644 --- a/Resources/Private/JavaScript/asset-tags/src/state/createTagDialogState.ts +++ b/Resources/Private/JavaScript/asset-tags/src/state/createTagDialogState.ts @@ -5,6 +5,11 @@ const createTagDialogState = atom({ default: { visible: false, label: '', + tags: [], + validation: { + valid: false, + errors: [], + }, }, }); diff --git a/Resources/Private/JavaScript/media-module/tests/tags.ts b/Resources/Private/JavaScript/media-module/tests/tags.ts index 52272f1aa..2955b3a13 100644 --- a/Resources/Private/JavaScript/media-module/tests/tags.ts +++ b/Resources/Private/JavaScript/media-module/tests/tags.ts @@ -1,8 +1,11 @@ import page from './page-model'; +import { ReactSelector } from 'testcafe-react-selectors'; import { SERVER_NAME } from './helpers'; fixture('Tags').page(SERVER_NAME); +const subSection = (name) => console.log('\x1b[33m%s\x1b[0m', ' - ' + name); + test('Clicking first tag updates list and only assets should be shown that are assigned to it', async (t) => { await t // Uncollapse the tag list @@ -19,3 +22,34 @@ test('Clicking first tag updates list and only assets should be shown that are a .expect(page.assetCount.innerText) .eql('12 assets'); }); + +test('Create a new tag and test validation', async (t) => { + subSection('Check existing tag label validation'); + await t + .click(page.assetCollections.withText('All')) + .click(page.collectionTree.findReact('AddTagButton')) + .typeText(ReactSelector('CreateTagDialog').findReact('TextInput'), 'Example tag 1') + .expect( + ReactSelector('CreateTagDialog') + .findReact('TextInput') + .withProps({ validationerrors: ['This input is invalid'] }).exists + ) + .ok('Text input should have validation errors') + .expect(ReactSelector('CreateTagDialog').findReact('Button').withProps({ disabled: true }).exists) + .ok('Create button should be disabled') + .expect(ReactSelector('CreateTagDialog').find('ul li').textContent) + .eql('A tag with this label already exists') + .typeText(ReactSelector('CreateTagDialog').findReact('TextInput'), '00') + .expect(ReactSelector('CreateTagDialog').find('ul li').exists) + .notOk('The tooltip should not be visible anymore') + .expect(ReactSelector('CreateTagDialog').findReact('Button').withProps({ disabled: false }).exists) + .ok('Create button should be enabled'); + + subSection('Check emtpy tag label validation'); + await t + .typeText(ReactSelector('CreateTagDialog').findReact('TextInput'), ' ', { replace: true }) + .expect(ReactSelector('CreateTagDialog').findReact('Button').withProps({ disabled: true }).exists) + .ok('Create button should be disabled') + .expect(ReactSelector('CreateTagDialog').find('ul li').textContent) + .eql('Please provide a tag label'); +}); diff --git a/Resources/Private/Translations/de/Main.xlf b/Resources/Private/Translations/de/Main.xlf index 1b985d9e1..39768f730 100644 --- a/Resources/Private/Translations/de/Main.xlf +++ b/Resources/Private/Translations/de/Main.xlf @@ -412,6 +412,14 @@ Create tag Tag erstellen + + Please provide a tag label + Bitte geben Sie einen Tag-Namen ein + + + This tag is already exists. Please choose a different one. + Dieser Tag existiert bereits. Bitte wählen Sie einen anderen aus. + Tag was created Tag wurde erstellt diff --git a/Resources/Private/Translations/en/Main.xlf b/Resources/Private/Translations/en/Main.xlf index 2cbd5f036..43b3b31fa 100644 --- a/Resources/Private/Translations/en/Main.xlf +++ b/Resources/Private/Translations/en/Main.xlf @@ -316,6 +316,12 @@ Tag was created + + Please provide a tag label + + + This tag is already exists. Please choose a different one. + Failed to create tag