diff --git a/cypress/e2e/item/share/changeVisibility.cy.ts b/cypress/e2e/item/share/changeVisibility.cy.ts index 72957ff24..57a891919 100644 --- a/cypress/e2e/item/share/changeVisibility.cy.ts +++ b/cypress/e2e/item/share/changeVisibility.cy.ts @@ -2,6 +2,7 @@ import { ItemLoginSchemaType, ItemTagType, PackedFolderItemFactory, + PublicationStatus, } from '@graasp/sdk'; import { buildItemPath } from '@/config/paths'; @@ -10,8 +11,11 @@ import { SETTINGS } from '../../../../src/config/constants'; import { SHARE_ITEM_PSEUDONYMIZED_SCHEMA_ID, SHARE_ITEM_VISIBILITY_SELECT_ID, + UPDATE_VISIBILITY_MODAL_VALIDATE_BUTTON, + buildDataCyWrapper, buildShareButtonId, } from '../../../../src/config/selectors'; +import { PublishedItemFactory } from '../../../fixtures/items'; const changeVisibility = (value: string): void => { cy.get(`#${SHARE_ITEM_VISIBILITY_SELECT_ID}`).click(); @@ -25,12 +29,12 @@ describe('Visibility of an Item', () => { cy.visit(buildItemPath(item.id)); cy.get(`#${buildShareButtonId(item.id)}`).click(); - const visiblitySelect = cy.get( + const visibilitySelect = cy.get( `#${SHARE_ITEM_VISIBILITY_SELECT_ID} + input`, ); // visibility select default value - visiblitySelect.should('have.value', SETTINGS.ITEM_PRIVATE.name); + visibilitySelect.should('have.value', SETTINGS.ITEM_PRIVATE.name); // change private -> public changeVisibility(SETTINGS.ITEM_PUBLIC.name); @@ -47,12 +51,12 @@ describe('Visibility of an Item', () => { cy.visit(buildItemPath(item.id)); cy.get(`#${buildShareButtonId(item.id)}`).click(); cy.wait(1000); - const visiblitySelect = cy.get( + const visibilitySelect = cy.get( `#${SHARE_ITEM_VISIBILITY_SELECT_ID} + input`, ); // visibility select default value - visiblitySelect.should('have.value', SETTINGS.ITEM_PUBLIC.name); + visibilitySelect.should('have.value', SETTINGS.ITEM_PUBLIC.name); // change public -> private changeVisibility(SETTINGS.ITEM_PRIVATE.name); @@ -69,12 +73,12 @@ describe('Visibility of an Item', () => { cy.visit(buildItemPath(item.id)); cy.get(`#${buildShareButtonId(item.id)}`).click(); cy.wait(1000); - const visiblitySelect = cy.get( + const visibilitySelect = cy.get( `#${SHARE_ITEM_VISIBILITY_SELECT_ID} + input`, ); // visibility select default value - visiblitySelect.should('have.value', SETTINGS.ITEM_PUBLIC.name); + visibilitySelect.should('have.value', SETTINGS.ITEM_PUBLIC.name); // change public -> item login changeVisibility(SETTINGS.ITEM_LOGIN.name); @@ -125,4 +129,71 @@ describe('Visibility of an Item', () => { expect(url).to.include(item.id); }); }); + + describe('Change visibility of published item', () => { + it('User should validate the change to private', () => { + const item = PublishedItemFactory( + PackedFolderItemFactory({}, { publicTag: {} }), + ); + cy.setUpApi({ + items: [item], + itemPublicationStatus: PublicationStatus.Published, + }); + cy.visit(buildItemPath(item.id)); + cy.get(`#${buildShareButtonId(item.id)}`).click(); + const visibilitySelect = cy.get( + `#${SHARE_ITEM_VISIBILITY_SELECT_ID} + input`, + ); + + // visibility select default value + visibilitySelect.should('have.value', SETTINGS.ITEM_PUBLIC.name); + + // try to change public -> private + changeVisibility(SETTINGS.ITEM_PRIVATE.name); + // the user have to confirm that changing visibility will remove the publication + cy.get( + `${buildDataCyWrapper(UPDATE_VISIBILITY_MODAL_VALIDATE_BUTTON)}`, + ).click(); + cy.wait(`@deleteItemTag-${ItemTagType.Public}`).then( + ({ request: { url } }) => { + expect(url).to.contain(item.id); + }, + ); + }); + + it('User should validate the change to item login', () => { + const item = PublishedItemFactory( + PackedFolderItemFactory({}, { publicTag: {} }), + ); + cy.setUpApi({ + items: [item], + itemPublicationStatus: PublicationStatus.Published, + }); + cy.visit(buildItemPath(item.id)); + cy.get(`#${buildShareButtonId(item.id)}`).click(); + const visibilitySelect = cy.get( + `#${SHARE_ITEM_VISIBILITY_SELECT_ID} + input`, + ); + + // visibility select default value + visibilitySelect.should('have.value', SETTINGS.ITEM_PUBLIC.name); + + // try to change public -> item login + changeVisibility(SETTINGS.ITEM_LOGIN.name); + // the user have to confirm that changing visibility will remove the publication + cy.get( + `${buildDataCyWrapper(UPDATE_VISIBILITY_MODAL_VALIDATE_BUTTON)}`, + ).click(); + cy.wait([ + `@deleteItemTag-${ItemTagType.Public}`, + '@putItemLoginSchema', + ]).then((data) => { + const { + request: { url }, + } = data[0]; + expect(url).to.contain(item.id); + expect(url).to.contain(ItemTagType.Public); // originally item login + }); + }); + }); }); diff --git a/src/components/item/sharing/UpdateVisibilityModal.tsx b/src/components/item/sharing/UpdateVisibilityModal.tsx new file mode 100644 index 000000000..a19368cd3 --- /dev/null +++ b/src/components/item/sharing/UpdateVisibilityModal.tsx @@ -0,0 +1,79 @@ +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Typography, +} from '@mui/material'; + +import { useBuilderTranslation } from '@/config/i18n'; +import { + UPDATE_VISIBILITY_MODAL_CANCEL_BUTTON, + UPDATE_VISIBILITY_MODAL_VALIDATE_BUTTON, +} from '@/config/selectors'; +import { BUILDER } from '@/langs/constants'; + +export type Visibility = { + name: string; + value: string; +}; + +type Props = { + isOpen: boolean; + newVisibility?: Visibility; + onClose: () => void; + onValidate: (visibility: string) => void; +}; + +export const UpdateVisibilityModal = ({ + isOpen, + newVisibility, + onClose, + onValidate, +}: Props): JSX.Element | null => { + const { t } = useBuilderTranslation(); + + if (!newVisibility) { + return null; + } + + const handleValidate = async () => { + onValidate(newVisibility.value); + }; + + return ( + + + + {t(BUILDER.UPDATE_VISIBILITY_MODAL_TITLE)} + + + + + {t(BUILDER.UPDATE_VISIBILITY_MODAL_DESCRIPTION)} + + + + + + + + ); +}; + +export default UpdateVisibilityModal; diff --git a/src/components/item/sharing/VisibilitySelect.hook.tsx b/src/components/item/sharing/VisibilitySelect.hook.tsx new file mode 100644 index 000000000..d27db292c --- /dev/null +++ b/src/components/item/sharing/VisibilitySelect.hook.tsx @@ -0,0 +1,82 @@ +import { useState } from 'react'; + +import { PublicationStatus } from '@graasp/sdk'; + +import { hooks } from '@/config/queryClient'; + +import { SETTINGS } from '../../../config/constants'; +import { useBuilderTranslation } from '../../../config/i18n'; +import { BUILDER } from '../../../langs/constants'; +import { Visibility } from './UpdateVisibilityModal'; + +const { usePublicationStatus } = hooks; + +type Props = { + itemId: string; + visibility?: string; + updateVisibility: (newVisibility: string) => void | Promise; +}; + +type UseVisibilitySelect = { + isModalOpen: boolean; + pendingVisibility: Visibility | undefined; + onCloseModal: () => void; + onValidateModal: (newVisibility: string) => void; + onVisibilityChange: (newVisibility: string) => void; +}; + +const useVisibilitySelect = ({ + itemId, + visibility, + updateVisibility, +}: Props): UseVisibilitySelect => { + const { t: translateBuilder } = useBuilderTranslation(); + const { data: publicationStatus } = usePublicationStatus(itemId); + + // The visibility value is temporary and awaits user confirmation through the dialog. + const [pendingVisibility, setPendingVisibility] = useState< + Visibility | undefined + >(); + + const translatedVisibilities = { + [SETTINGS.ITEM_LOGIN.name]: translateBuilder( + BUILDER.ITEM_SETTINGS_VISIBILITY_PSEUDONYMIZED_LABEL, + ), + [SETTINGS.ITEM_PUBLIC.name]: translateBuilder( + BUILDER.ITEM_SETTINGS_VISIBILITY_PUBLIC_INFORMATIONS, + ), + [SETTINGS.ITEM_PRIVATE.name]: translateBuilder( + BUILDER.ITEM_SETTINGS_VISIBILITY_PRIVATE_LABEL, + ), + }; + + const onVisibilityChange = (newVisibility: string) => { + if ( + visibility === SETTINGS.ITEM_PUBLIC.name && + publicationStatus === PublicationStatus.Published + ) { + setPendingVisibility({ + name: translatedVisibilities[newVisibility], + value: newVisibility, + }); + } else { + updateVisibility(newVisibility); + } + }; + + const onCloseModal = () => setPendingVisibility(undefined); + const onValidateModal = (newVisibility: string) => { + onCloseModal(); + updateVisibility(newVisibility); + }; + + return { + isModalOpen: Boolean(pendingVisibility), + pendingVisibility, + onCloseModal, + onValidateModal, + onVisibilityChange, + }; +}; + +export default useVisibilitySelect; diff --git a/src/components/item/sharing/VisibilitySelect.tsx b/src/components/item/sharing/VisibilitySelect.tsx index c931f7a7b..fa796ee0b 100644 --- a/src/components/item/sharing/VisibilitySelect.tsx +++ b/src/components/item/sharing/VisibilitySelect.tsx @@ -10,6 +10,8 @@ import { useBuilderTranslation } from '../../../config/i18n'; import { SHARE_ITEM_VISIBILITY_SELECT_ID } from '../../../config/selectors'; import { BUILDER } from '../../../langs/constants'; import ItemLoginSchemaSelect from './ItemLoginSchemaSelect'; +import UpdateVisibilityModal from './UpdateVisibilityModal'; +import useVisibilitySelect from './VisibilitySelect.hook'; type Props = { item: PackedItem; @@ -28,6 +30,18 @@ const VisibilitySelect = ({ item, edit }: Props): JSX.Element | null => { updateVisibility, } = useVisibility(item); + const { + isModalOpen, + pendingVisibility, + onCloseModal, + onValidateModal, + onVisibilityChange, + } = useVisibilitySelect({ + itemId: item.id, + visibility, + updateVisibility, + }); + if (isLoading) { return ; } @@ -68,10 +82,18 @@ const VisibilitySelect = ({ item, edit }: Props): JSX.Element | null => { return ( <> + {isModalOpen && ( + + )} {edit && (