Skip to content
This repository was archived by the owner on Jan 23, 2025. It is now read-only.

feat: prevent changing visibility published item #1359

Merged
merged 1 commit into from
Jul 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
feat: show modal when changing visibility of published item
  • Loading branch information
ReidyT committed Jul 22, 2024
commit 4fa2a8f453f528fc188462456259630fff607f5c
83 changes: 77 additions & 6 deletions cypress/e2e/item/share/changeVisibility.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
ItemLoginSchemaType,
ItemTagType,
PackedFolderItemFactory,
PublicationStatus,
} from '@graasp/sdk';

import { buildItemPath } from '@/config/paths';
Expand All @@ -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();
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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
});
});
});
});
79 changes: 79 additions & 0 deletions src/components/item/sharing/UpdateVisibilityModal.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Dialog open={isOpen}>
<DialogTitle>
<Typography variant="h3">
{t(BUILDER.UPDATE_VISIBILITY_MODAL_TITLE)}
</Typography>
</DialogTitle>
<DialogContent>
<Typography>
{t(BUILDER.UPDATE_VISIBILITY_MODAL_DESCRIPTION)}
</Typography>
</DialogContent>
<DialogActions>
<Button
data-cy={UPDATE_VISIBILITY_MODAL_CANCEL_BUTTON}
onClick={onClose}
variant="outlined"
>
{t(BUILDER.CANCEL_BUTTON)}
</Button>
<Button
data-cy={UPDATE_VISIBILITY_MODAL_VALIDATE_BUTTON}
onClick={handleValidate}
variant="contained"
>
{t(BUILDER.UPDATE_VISIBILITY_MODAL_VALIDATE_BUTTON, {
visibility: newVisibility.name,
})}
</Button>
</DialogActions>
</Dialog>
);
};

export default UpdateVisibilityModal;
82 changes: 82 additions & 0 deletions src/components/item/sharing/VisibilitySelect.hook.tsx
Original file line number Diff line number Diff line change
@@ -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<void>;
};

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;
24 changes: 23 additions & 1 deletion src/components/item/sharing/VisibilitySelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 <Loader />;
}
Expand Down Expand Up @@ -68,10 +82,18 @@ const VisibilitySelect = ({ item, edit }: Props): JSX.Element | null => {

return (
<>
{isModalOpen && (
<UpdateVisibilityModal
isOpen={isModalOpen}
newVisibility={pendingVisibility}
onClose={onCloseModal}
onValidate={onValidateModal}
/>
)}
{edit && (
<Select
value={visibility}
onChange={(e) => updateVisibility(e.target.value)}
onChange={(e) => onVisibilityChange(e.target.value)}
disabled={isDisabled}
id={SHARE_ITEM_VISIBILITY_SELECT_ID}
sx={{ mr: 1 }}
Expand Down
4 changes: 4 additions & 0 deletions src/config/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,10 @@ export const buildPublicationStatus = (status: PublicationStatus): string =>

export const PUBLIC_VISIBILITY_MODAL_VALIDATE_BUTTON =
'publicVisbilityModalValidateButton';
export const UPDATE_VISIBILITY_MODAL_VALIDATE_BUTTON =
'updateVisbilityModalValidateButton';
export const UPDATE_VISIBILITY_MODAL_CANCEL_BUTTON =
'updateVisbilityModalCancelButton';

export const DEBOUNCED_TEXT_FIELD_ID = 'debouncedTextfield';

Expand Down
5 changes: 5 additions & 0 deletions src/langs/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,11 @@ export const BUILDER = {
PUBLIC_VISIBILITY_MODAL_VALIDATE_BUTTON:
'PUBLIC_VISIBILITY_MODAL_VALIDATE_BUTTON',

UPDATE_VISIBILITY_MODAL_TITLE: 'UPDATE_VISIBILITY_MODAL_TITLE',
UPDATE_VISIBILITY_MODAL_DESCRIPTION: 'UPDATE_VISIBILITY_MODAL_DESCRIPTION',
UPDATE_VISIBILITY_MODAL_VALIDATE_BUTTON:
'UPDATE_VISIBILITY_MODAL_VALIDATE_BUTTON',

PUBLISHED_ITEMS_TITLE: 'PUBLISHED_ITEMS_TITLE',

SHARE_ITEM_CSV_IMPORT_MODAL_TITLE: 'SHARE_ITEM_CSV_IMPORT_MODAL_TITLE',
Expand Down
3 changes: 3 additions & 0 deletions src/langs/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,9 @@
"PUBLIC_VISIBILITY_MODAL_TITLE": "Item visibility",
"PUBLIC_VISIBILITY_MODAL_DESCRIPTION": "The visibility of this item is not Public. To publish it in the Library, you must set its visibility to pubic. This action will allow any user to have access to your item. Do you want to continue?",
"PUBLIC_VISIBILITY_MODAL_VALIDATE_BUTTON": "Make it public",
"UPDATE_VISIBILITY_MODAL_TITLE": "Change visibility of published item",
"UPDATE_VISIBILITY_MODAL_DESCRIPTION": "This element is currently published in the Graasp library. If you change its visibility, the item will be unpublished and will no longer be available in the library. Would you like to continue?",
"UPDATE_VISIBILITY_MODAL_VALIDATE_BUTTON": "Make it {{visibility}}",
"LINK_DEFAULT_NAME": "My Link",
"MOVE_BUTTON": "Move",
"MOVE_BUTTON_zero": "Move",
Expand Down