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

feat: don't allow none folder items to be published #1284

Merged
merged 1 commit into from
Jun 13, 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
45 changes: 45 additions & 0 deletions cypress/e2e/item/publish/publishedItem.cy.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import {
ItemTagType,
ItemType,
ItemTypeUnion,
ItemValidationGroup,
ItemValidationStatus,
Member,
PackedFolderItemFactory,
PackedItem,
PermissionLevel,
PublishableItemTypeChecker,
} from '@graasp/sdk';

import { PublicationStatus } from '@/types/publication';
Expand All @@ -24,6 +27,7 @@ import {
PublishedItemFactory,
} from '../../../fixtures/items';
import { MEMBERS } from '../../../fixtures/members';
import { createPublicItemByType } from '../../../fixtures/publish/publish';
import { ItemForTest } from '../../../support/types';

const openPublishItemTab = (id: string) => {
Expand Down Expand Up @@ -340,4 +344,45 @@ describe('Public Item', () => {
waitOnUnpublishItem(publicItem);
});
});

describe('Only authorized types can be published', () => {
const testItemType = (
testTitle: string,
item: ItemForTest,
statusExpected: PublicationStatus,
) => {
it(testTitle, () => {
setUpAndVisitItemPage(item);
openPublishItemTab(item.id);
getPublicationStatusComponent(statusExpected)
.should('exist')
.should('be.visible');
});
};

const testAuthorizedType = (item: ItemForTest) => {
testItemType(
`Publication should be allowed for type "${item.type}"`,
item,
PublicationStatus.Unpublished,
);
};

const testUnauthorizedType = (item: ItemForTest) => {
testItemType(
`Publication should NOT be allowed for type "${item.type}"`,
item,
PublicationStatus.ItemTypeNotAllowed,
);
};

Object.values(ItemType).forEach((itemType: ItemTypeUnion) => {
const item = createPublicItemByType(itemType);
if (PublishableItemTypeChecker.isItemTypeAllowedToBePublished(itemType)) {
testAuthorizedType(item);
} else {
testUnauthorizedType(item);
}
});
});
});
47 changes: 47 additions & 0 deletions cypress/fixtures/publish/publish.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {
ItemTypeUnion,
PackedAppItemFactory,
PackedDocumentItemFactory,
PackedEtherpadItemFactory,
PackedFolderItemFactory,
PackedH5PItemFactory,
PackedLinkItemFactory,
PackedLocalFileItemFactory,
PackedS3FileItemFactory,
PackedShortcutItemFactory,
} from '@graasp/sdk';

import { ItemForTest } from '../../support/types';

export const createPublicItemByType = (
itemType: ItemTypeUnion,
): ItemForTest => {
const publicTag = { publicTag: {} };

switch (itemType) {
case 'app':
return PackedAppItemFactory({}, publicTag);
case 'document':
return PackedDocumentItemFactory({}, publicTag);
case 'folder':
return PackedFolderItemFactory({}, publicTag);
case 'embeddedLink':
return PackedLinkItemFactory({}, publicTag);
case 'file':
return PackedLocalFileItemFactory({}, publicTag);
case 's3File':
return PackedS3FileItemFactory({}, publicTag);
case 'shortcut':
return PackedShortcutItemFactory({}, publicTag);
case 'h5p':
return PackedH5PItemFactory({}, publicTag);
case 'etherpad':
return PackedEtherpadItemFactory({}, publicTag);
default:
throw new Error(
`Item Type "${itemType}" is unknown in "createPublicItemWithType"`,
);
}
};

export default createPublicItemByType;
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"@graasp/chatbox": "3.1.0",
"@graasp/map": "1.15.0",
"@graasp/query-client": "3.13.0",
"@graasp/sdk": "4.12.1",
"@graasp/sdk": "4.13.0",
"@graasp/translations": "1.28.0",
"@graasp/ui": "4.19.3",
"@mui/icons-material": "5.15.19",
Expand Down
7 changes: 7 additions & 0 deletions src/components/hooks/usePublicationStatus.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import { useEffect, useState } from 'react';

import {
ItemPublished,
ItemTypeUnion,
ItemValidation,
ItemValidationGroup,
ItemValidationStatus,
PackedItem,
PublishableItemTypeChecker,
} from '@graasp/sdk';

import groupBy from 'lodash.groupby';
Expand Down Expand Up @@ -59,6 +61,9 @@ const isPublishedChildren = ({
publishedEntry?: ItemPublished;
}) => Boolean(publishedEntry) && publishedEntry?.item?.path !== item?.path;

const isTypeNotAllowedToBePublished = (itemType: ItemTypeUnion) =>
!PublishableItemTypeChecker.isItemTypeAllowedToBePublished(itemType);

type Props = { item: PackedItem };
type UsePublicationStatus = {
status: PublicationStatus;
Expand All @@ -83,6 +88,8 @@ const computePublicationStatus = ({
switch (true) {
case isPublishedChildren({ item, publishedEntry }):
return PublicationStatus.PublishedChildren;
case isTypeNotAllowedToBePublished(item.type):
return PublicationStatus.ItemTypeNotAllowed;
case isUnpublished(validationGroup):
return PublicationStatus.Unpublished;
case isValidationOutdated({ item, validationGroup }):
Expand Down
8 changes: 6 additions & 2 deletions src/components/item/publish/CoEditorsContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ export const CoEditorsContainer = ({
settings.displayCoEditors ?? false,
);

const hiddenStatus = [
PublicationStatus.PublishedChildren,
PublicationStatus.ItemTypeNotAllowed,
];

const {
mutate: updateDisplayCoEditors,
isLoading,
Expand Down Expand Up @@ -68,8 +73,7 @@ export const CoEditorsContainer = ({
const handleNotifyCoEditorsChange = (isChecked: boolean): void =>
onNotificationChanged(isChecked);

// The publication is managed by the parent
if (status === PublicationStatus.PublishedChildren) {
if (hiddenStatus.includes(status)) {
return null;
}

Expand Down
47 changes: 39 additions & 8 deletions src/components/item/publish/ItemPublishTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
DataSyncContextProvider,
useDataSyncContext,
} from '@/components/context/DataSyncContext';
import usePublicationStatus from '@/components/hooks/usePublicationStatus';
import CategoriesContainer from '@/components/item/publish/CategoriesContainer';
import CoEditorsContainer from '@/components/item/publish/CoEditorsContainer';
import EditItemDescription from '@/components/item/publish/EditItemDescription';
Expand All @@ -22,6 +23,7 @@ import { OutletType } from '@/components/pages/item/type';
import { useBuilderTranslation } from '@/config/i18n';
import { BUILDER } from '@/langs/constants';
import { SomeBreakPoints } from '@/types/breakpoint';
import { PublicationStatus } from '@/types/publication';

import EditItemName from './EditItemName';
import CustomizedTags from './customizedTags/CustomizedTags';
Expand All @@ -35,11 +37,26 @@ const ItemPublishTab = (): JSX.Element => {
const { isLoading: isMemberLoading } = useCurrentUserContext();
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
const { status } = useDataSyncContext();
const {
status: publicationStatus,
isinitialLoading: isPublicationStatusLoading,
} = usePublicationStatus({
item,
});

const [notifyCoEditors, setNotifyCoEditors] = useState<boolean>(false);

if (isMemberLoading) {
return <Loader />;
if (isMemberLoading || isPublicationStatusLoading) {
return (
<Stack
alignItems="center"
justifyContent="center"
width="100%"
height="70vh"
>
<Loader />
</Stack>
);
}

if (!canAdmin) {
Expand Down Expand Up @@ -117,8 +134,19 @@ const ItemPublishTab = (): JSX.Element => {
</Stack>
);

return (
<Container disableGutters sx={{ mt: 2 }}>
const buildPublicationStack = (): JSX.Element => (
<Stack flexBasis="100%" spacing={2}>
{buildPublicationHeader()}
{buildPublicationSection()}
</Stack>
);

const buildView = () => {
if (publicationStatus === PublicationStatus.ItemTypeNotAllowed) {
return buildPublicationStack();
}

return (
<Stack direction={{ xs: 'column', md: 'row' }} gap={6}>
{buildPreviewSection({ order: { xs: 1, md: 0 } })}
{isMobile ? (
Expand All @@ -127,12 +155,15 @@ const ItemPublishTab = (): JSX.Element => {
{buildPublicationSection({ order: { xs: 2 } })}
</>
) : (
<Stack flexBasis="100%" spacing={2}>
{buildPublicationHeader()}
{buildPublicationSection()}
</Stack>
buildPublicationStack()
)}
</Stack>
);
};

return (
<Container disableGutters sx={{ mt: 2 }}>
{buildView()}
</Container>
);
};
Expand Down
20 changes: 17 additions & 3 deletions src/components/item/publish/PublicationStatusComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,24 @@ import CloudOffIcon from '@mui/icons-material/CloudOff';
import CloudUploadIcon from '@mui/icons-material/CloudUpload';
import ErrorIcon from '@mui/icons-material/Error';
import EventBusyIcon from '@mui/icons-material/EventBusy';
import InfoIcon from '@mui/icons-material/Info';
import PendingActionsIcon from '@mui/icons-material/PendingActions';
import PublicOffIcon from '@mui/icons-material/PublicOff';
import { Chip, ChipProps, CircularProgress } from '@mui/material';

import { PackedItem } from '@graasp/sdk';

import { useBuilderTranslation } from '@/config/i18n';
import { useBuilderTranslation, useEnumsTranslation } from '@/config/i18n';
import { buildPublicationStatus } from '@/config/selectors';
import { BUILDER } from '@/langs/constants';
import { PublicationStatus, PublicationStatusMap } from '@/types/publication';

import usePublicationStatus from '../../hooks/usePublicationStatus';

function capitalizeFirstLetter(text: string) {
return text.charAt(0).toUpperCase() + text.slice(1);
}

type PublicationComponentMap = PublicationStatusMap<{
icon: JSX.Element;
label: string;
Expand All @@ -28,7 +33,9 @@ type Props = {

export const PublicationStatusComponent = ({ item }: Props): JSX.Element => {
const { t } = useBuilderTranslation();
const { t: translateEnum } = useEnumsTranslation();
const { status, isinitialLoading } = usePublicationStatus({ item });
const translatedType = capitalizeFirstLetter(translateEnum(item.type));

if (isinitialLoading) {
return (
Expand All @@ -55,12 +62,12 @@ export const PublicationStatusComponent = ({ item }: Props): JSX.Element => {
[PublicationStatus.Pending]: {
icon: <PendingActionsIcon />,
label: t(BUILDER.LIBRARY_SETTINGS_PUBLICATION_STATUS_PENDING),
color: 'warning',
color: 'info',
},
[PublicationStatus.ReadyToPublish]: {
icon: <CloudUploadIcon />,
label: t(BUILDER.LIBRARY_SETTINGS_PUBLICATION_STATUS_READY_TO_PUBLISH),
color: 'info',
color: 'success',
},
[PublicationStatus.NotPublic]: {
icon: <PublicOffIcon />,
Expand All @@ -82,6 +89,13 @@ export const PublicationStatusComponent = ({ item }: Props): JSX.Element => {
label: t(BUILDER.LIBRARY_SETTINGS_PUBLICATION_STATUS_UNPUBLISHED),
color: undefined,
},
[PublicationStatus.ItemTypeNotAllowed]: {
icon: <InfoIcon />,
label: t(BUILDER.LIBRARY_SETTINGS_PUBLICATION_STATUS_TYPE_NOT_ALLOWED, {
itemType: translatedType,
}),
color: 'info',
},
} as const;

const { icon, label, color } = chipMap[status];
Expand Down
12 changes: 8 additions & 4 deletions src/components/item/publish/publicationButtons/InvalidButton.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { LoadingButton } from '@mui/lab';
import { Alert, LoadingButton } from '@mui/lab';

import { PackedItem } from '@graasp/sdk';

Expand Down Expand Up @@ -41,9 +41,13 @@ export const InvalidButton = ({ item, isLoading }: Props): JSX.Element => {
closeModal();
};

const description = t(BUILDER.LIBRARY_SETTINGS_VALIDATION_STATUS_FAILURE, {
contact: ADMIN_CONTACT,
});
const description = (
<Alert severity="error">
{t(BUILDER.LIBRARY_SETTINGS_VALIDATION_STATUS_FAILURE, {
contact: ADMIN_CONTACT,
})}
</Alert>
);

return (
<>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Alert } from '@mui/material';

import { PublishableItemTypeChecker } from '@graasp/sdk';

import { useBuilderTranslation, useEnumsTranslation } from '@/config/i18n';
import { BUILDER } from '@/langs/constants';

export const NotAllowedItemTypeButton = (): JSX.Element => {
const { t } = useBuilderTranslation();
const { t: translateEnum } = useEnumsTranslation();

const allowedTypes = PublishableItemTypeChecker.getAllowedTypes();
const translatedAllowedTypes = allowedTypes
.map((e) => translateEnum(e))
.join(', ');

return (
<Alert severity="info">
{t(BUILDER.LIBRARY_SETTINGS_TYPE_NOT_ALLOWED_STATUS, {
allowedItemTypes: translatedAllowedTypes,
count: allowedTypes.length,
})}
</Alert>
);
};

export default NotAllowedItemTypeButton;
Loading