Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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
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