Skip to content

Commit

Permalink
feat: restrict publication to folders only
Browse files Browse the repository at this point in the history
  - feat: improve the description components of the status button
  - feat: hide publication preview if type cannot be published
  - chore(dep): update sdk to 4.13.0
  • Loading branch information
ReidyT committed Jun 13, 2024
1 parent 3c1d5e2 commit 5871b40
Show file tree
Hide file tree
Showing 20 changed files with 241 additions and 47 deletions.
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

0 comments on commit 5871b40

Please sign in to comment.