diff --git a/cypress/e2e/pinned.cy.ts b/cypress/e2e/pinned.cy.ts index 4a6c4d8b..e05fa49c 100644 --- a/cypress/e2e/pinned.cy.ts +++ b/cypress/e2e/pinned.cy.ts @@ -65,7 +65,7 @@ describe('Pinned Items', () => { cy.visit(buildMainPath({ rootId: parent.id })); cy.wait(['@getChildren']); - cy.get(`#${ITEM_PINNED_BUTTON_ID}`).should('be.disabled'); + cy.get(`#${ITEM_PINNED_BUTTON_ID}`).should('not.exist'); cy.get(`#${ITEM_PINNED_ID}`).should('not.exist'); }); }); diff --git a/cypress/fixtures/documents.ts b/cypress/fixtures/documents.ts index 31602201..fbfa4f27 100644 --- a/cypress/fixtures/documents.ts +++ b/cypress/fixtures/documents.ts @@ -1,7 +1,6 @@ import { DocumentItemFactory, DocumentItemType, - ItemTagType, ItemType, PermissionLevel, buildDocumentExtra, @@ -9,7 +8,7 @@ import { import { CURRENT_USER, MEMBERS } from './members'; import { MockItem } from './mockTypes'; -import { mockItemTag } from './tags'; +import { mockHiddenTag, mockPublicTag } from './tags'; export const GRAASP_DOCUMENT_ITEM: DocumentItemType = DocumentItemFactory({ id: 'ecafbd2a-5688-12eb-ae91-0242ac130002', @@ -59,7 +58,7 @@ export const GRAASP_DOCUMENT_ITEM_HIDDEN: MockItem = { showChatbox: false, }, }), - tags: [mockItemTag({ type: ItemTagType.Hidden })], + hidden: mockHiddenTag(), memberships: [{ memberId: MEMBERS.BOB.id, permission: PermissionLevel.Read }], }; @@ -78,7 +77,7 @@ export const GRAASP_DOCUMENT_ITEM_PUBLIC_VISIBLE: MockItem = { showChatbox: false, }, }), - tags: [mockItemTag({ type: ItemTagType.Public })], + public: mockPublicTag(), }; export const GRAASP_DOCUMENT_ITEM_PUBLIC_HIDDEN: MockItem = { @@ -97,10 +96,8 @@ export const GRAASP_DOCUMENT_ITEM_PUBLIC_HIDDEN: MockItem = { showChatbox: false, }, }), - tags: [ - mockItemTag({ type: ItemTagType.Public }), - mockItemTag({ type: ItemTagType.Hidden }), - ], + hidden: mockHiddenTag(), + public: mockPublicTag(), }; export const GRAASP_DOCUMENT_ITEM_WITH_CHAT_BOX: DocumentItemType = diff --git a/cypress/fixtures/items.ts b/cypress/fixtures/items.ts index 86a8db49..1381b15f 100644 --- a/cypress/fixtures/items.ts +++ b/cypress/fixtures/items.ts @@ -1,9 +1,4 @@ -import { - ItemTagType, - ItemType, - PermissionLevel, - buildPathFromIds, -} from '@graasp/sdk'; +import { ItemType, PermissionLevel, buildPathFromIds } from '@graasp/sdk'; import { DEFAULT_LANG } from '@graasp/translations'; import { v4 } from 'uuid'; @@ -17,7 +12,7 @@ import { } from './documents'; import { CURRENT_USER, MEMBERS } from './members'; import { MockItem } from './mockTypes'; -import { mockItemTag } from './tags'; +import { mockHiddenTag, mockPublicTag } from './tags'; export const DEFAULT_FOLDER_ITEM: MockItem = { description: '', @@ -25,6 +20,7 @@ export const DEFAULT_FOLDER_ITEM: MockItem = { name: '', displayName: 'default Display Name', path: '', + type: ItemType.FOLDER, extra: { [ItemType.FOLDER]: { childrenOrder: [], @@ -33,7 +29,6 @@ export const DEFAULT_FOLDER_ITEM: MockItem = { creator: CURRENT_USER, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), - type: ItemType.FOLDER, lang: DEFAULT_LANG, settings: { isPinned: false, @@ -100,6 +95,7 @@ export const FOLDER_WITH_SUBFOLDER_ITEM: { items: MockItem[] } = { id: 'ecafbd2a-5688-11eb-ae93-0242ac130002', name: 'parent folder', path: 'ecafbd2a_5688_11eb_ae93_0242ac130002', + type: ItemType.FOLDER, extra: { [ItemType.FOLDER]: { childrenOrder: [], @@ -271,7 +267,7 @@ export const PINNED_AND_HIDDEN_ITEM: { items: MockItem[] } = { isPinned: true, showChatbox: false, }, - tags: [mockItemTag({ type: ItemTagType.Hidden })], + hidden: mockHiddenTag(), }, { id: 'fdf09f5a-5688-11eb-ae93-0242ac130008', @@ -304,7 +300,7 @@ export const PUBLIC_FOLDER_WITH_PINNED_ITEMS: { items: MockItem[] } = { isPinned: false, showChatbox: false, }, - tags: [mockItemTag({ type: ItemTagType.Public })], + public: mockPublicTag(), }, { ...DEFAULT_FOLDER_ITEM, @@ -324,7 +320,7 @@ export const PUBLIC_FOLDER_WITH_PINNED_ITEMS: { items: MockItem[] } = { icons: [], }, }, - tags: [mockItemTag({ type: ItemTagType.Public })], + public: mockPublicTag(), }, { ...DEFAULT_FOLDER_ITEM, @@ -335,7 +331,7 @@ export const PUBLIC_FOLDER_WITH_PINNED_ITEMS: { items: MockItem[] } = { isPinned: true, showChatbox: false, }, - tags: [mockItemTag({ type: ItemTagType.Public })], + public: mockPublicTag(), }, ], }; @@ -363,7 +359,7 @@ export const FOLDER_WITH_HIDDEN_ITEMS: { items: MockItem[] } = { isPinned: false, showChatbox: false, }, - tags: [mockItemTag({ type: ItemTagType.Hidden })], + hidden: mockHiddenTag(), }, ], }; @@ -409,7 +405,7 @@ export const PUBLIC_FOLDER_WITH_HIDDEN_ITEMS: { items: MockItem[] } = { isPinned: false, showChatbox: false, }, - tags: [mockItemTag({ type: ItemTagType.Public })], + public: mockPublicTag(), }, GRAASP_DOCUMENT_ITEM_PUBLIC_VISIBLE, GRAASP_DOCUMENT_ITEM_PUBLIC_HIDDEN, diff --git a/cypress/fixtures/mockTypes.ts b/cypress/fixtures/mockTypes.ts index 9b0aef36..4f793ae4 100644 --- a/cypress/fixtures/mockTypes.ts +++ b/cypress/fixtures/mockTypes.ts @@ -1,11 +1,12 @@ -import { DiscriminatedItem, ItemTag, PermissionLevel } from '@graasp/sdk'; +import { ItemTag, PackedItem, PermissionLevel } from '@graasp/sdk'; export type MockItemTag = Omit; -export type MockItem = DiscriminatedItem & { +export type MockItem = Omit & { // for testing filepath?: string; // path to a fixture file in cypress filefixture?: string; memberships?: { memberId: string; permission: PermissionLevel }[]; - tags?: MockItemTag[]; + hidden?: MockItemTag; + public?: MockItemTag; }; diff --git a/cypress/fixtures/tags.ts b/cypress/fixtures/tags.ts index a8af9c91..24bf33c0 100644 --- a/cypress/fixtures/tags.ts +++ b/cypress/fixtures/tags.ts @@ -5,7 +5,6 @@ import { v4 } from 'uuid'; import { CURRENT_USER } from './members'; import { MockItemTag } from './mockTypes'; -// eslint-disable-next-line import/prefer-default-export export const mockItemTag = ({ creator = CURRENT_USER, type, @@ -15,6 +14,10 @@ export const mockItemTag = ({ }): MockItemTag => ({ id: v4(), type, - createdAt: new Date(), + createdAt: new Date().toISOString(), creator, }); +export const mockHiddenTag = (creator?: Member): MockItemTag => + mockItemTag({ creator, type: ItemTagType.Hidden }); +export const mockPublicTag = (creator?: Member): MockItemTag => + mockItemTag({ creator, type: ItemTagType.Public }); diff --git a/src/modules/common/HiddenWrapper.tsx b/src/modules/common/HiddenWrapper.tsx deleted file mode 100644 index 47a5a62c..00000000 --- a/src/modules/common/HiddenWrapper.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { Box, styled } from '@mui/material'; - -import { DiscriminatedItem } from '@graasp/sdk'; - -import { usePlayerTranslation } from '@/config/i18n'; -import { buildHiddenWrapperId } from '@/config/selectors'; -import { PLAYER } from '@/langs/constants'; - -export const HIDDEN_STYLE = { - backgroundColor: '#eee', - color: 'rgba(0, 0, 0, 0.3)', - padding: '16px', - opacity: '0.5', -}; - -const StyledBox = styled(Box, { - shouldForwardProp: (prop) => prop !== 'isHidden', -})<{ isHidden: boolean }>(({ isHidden }) => ({ - ...(isHidden ? HIDDEN_STYLE : {}), -})); - -const HiddenWrapper = ({ - hidden: isHidden, - itemId, - children, -}: { - hidden: boolean; - itemId: DiscriminatedItem['id']; - children: JSX.Element; -}): JSX.Element => { - const { t } = usePlayerTranslation(); - return ( - - {children} - - ); -}; - -export default HiddenWrapper; diff --git a/src/modules/common/ItemThumbnail.tsx b/src/modules/common/ItemThumbnail.tsx index 16052587..ea859998 100644 --- a/src/modules/common/ItemThumbnail.tsx +++ b/src/modules/common/ItemThumbnail.tsx @@ -27,7 +27,6 @@ const ItemThumbnail = ({ mimetype={getMimetype(item.extra)} alt={item.name} iconSrc={thumbnailSrc} - sx={{ borderRadius: 1 }} /> ); }; diff --git a/src/modules/item/Item.tsx b/src/modules/item/Item.tsx index e9c4fc5b..66efac24 100644 --- a/src/modules/item/Item.tsx +++ b/src/modules/item/Item.tsx @@ -17,6 +17,7 @@ import { ItemType, LinkItemType, LocalFileItemType, + PackedItem, PermissionLevel, S3FileItemType, ShortcutItemType, @@ -56,7 +57,7 @@ import { } from '@/config/selectors'; import { useCurrentMemberContext } from '@/contexts/CurrentMemberContext'; import { PLAYER } from '@/langs/constants'; -import { isHidden, paginationContentFilter } from '@/utils/item'; +import { paginationContentFilter } from '@/utils/item'; import NavigationIsland from '../navigationIsland/NavigationIsland'; import SectionHeader from './SectionHeader'; @@ -67,7 +68,6 @@ const { useItem, useChildren, useFileContentUrl, - useItemTags, useChildrenPaginated, } = hooks; @@ -359,12 +359,9 @@ const ItemContent = ({ item }: ItemContentProps) => { } }; -const ItemContentWrapper = ({ item }: { item: DiscriminatedItem }) => { - const { data: itemTags } = useItemTags(item.id); - const isItemHidden = isHidden(item, itemTags); - +const ItemContentWrapper = ({ item }: { item: PackedItem }) => { // An item the user has access to can be hidden (write, admin) so we hide it in player - if (isItemHidden) { + if (item.hidden) { return null; } return ; diff --git a/src/modules/navigation/ItemNavigation.tsx b/src/modules/navigation/ItemNavigation.tsx index 1a25e1f6..2bc727b8 100644 --- a/src/modules/navigation/ItemNavigation.tsx +++ b/src/modules/navigation/ItemNavigation.tsx @@ -12,12 +12,11 @@ import { axios, hooks } from '@/config/queryClient'; import { MAIN_MENU_ID, TREE_VIEW_ID } from '@/config/selectors'; import { useCurrentMemberContext } from '@/contexts/CurrentMemberContext.tsx'; import TreeView from '@/modules/navigation/tree/TreeView'; -import { isHidden } from '@/utils/item'; import { combineUuids, shuffleAllButLastItemInArray } from '@/utils/shuffle.ts'; import LoadingTree from './tree/LoadingTree'; -const { useItem, useDescendants, useItemsTags } = hooks; +const { useItem, useDescendants } = hooks; const DrawerNavigation = (): JSX.Element | null => { const rootId = useParams()[ROOT_ID_PATH]; @@ -38,7 +37,6 @@ const DrawerNavigation = (): JSX.Element | null => { const { isInitialLoading: isLoadingTree } = useDescendants({ id: rootId ?? '', }); - const { data: itemsTags } = useItemsTags(descendants?.map(({ id }) => id)); const { data: rootItem, isLoading, isError, error } = useItem(rootId); const handleNavigationOnClick = (newItemId: string) => { @@ -76,9 +74,7 @@ const DrawerNavigation = (): JSX.Element | null => { !isHidden(ele, itemsTags?.data?.[ele.id]), - )} + items={[rootItem, ...descendants].filter((ele) => !ele.hidden)} firstLevelStyle={{ fontWeight: 'bold' }} onTreeItemSelect={handleNavigationOnClick} /> diff --git a/src/modules/navigation/tree/Node.tsx b/src/modules/navigation/tree/Node.tsx index c5783093..5ccc7725 100644 --- a/src/modules/navigation/tree/Node.tsx +++ b/src/modules/navigation/tree/Node.tsx @@ -45,7 +45,7 @@ const Node = ({ {/* icon type for root level items */} {level === 1 && element.metadata?.type && ( { const { t } = usePlayerTranslation(); - const { itemId } = useParams(); + const { itemId, rootId } = useParams(); const { data: item } = hooks.useItem(itemId); + const { data: root } = hooks.useItem(rootId); + const { data: descendants } = hooks.useDescendants({ id: rootId }); const { toggleChatbox, isChatboxOpen } = useLayoutContext(); - const canWrite = item?.permission - ? PermissionLevelCompare.gte(item?.permission, PermissionLevel.Write) - : false; - - const isDisabled = !item?.settings?.showChatbox; - const tooltip = canWrite - ? t(PLAYER.NAVIGATION_ISLAND_CHAT_BUTTON_HELPER_TEXT_WRITERS) - : t(PLAYER.NAVIGATION_ISLAND_CHAT_BUTTON_HELPER_TEXT_READERS); - - return { - chatButton: ( - - - - {isChatboxOpen ? : } - - - - ), - }; + + const chatEnabledItems = descendants?.filter( + ({ settings }) => settings.showChatbox, + ); + + if ((chatEnabledItems ?? []).length > 0 || root?.settings.showChatbox) { + const canWrite = item?.permission + ? PermissionLevelCompare.gte(item?.permission, PermissionLevel.Write) + : false; + + const isDisabled = !item?.settings?.showChatbox; + const tooltip = canWrite + ? t(PLAYER.NAVIGATION_ISLAND_CHAT_BUTTON_HELPER_TEXT_WRITERS) + : t(PLAYER.NAVIGATION_ISLAND_CHAT_BUTTON_HELPER_TEXT_READERS); + + return { + chatButton: ( + + + + {isChatboxOpen ? : } + + + + ), + }; + } + // disable the chat button if there are no items with the chat enabled + return { chatButton: false }; }; export default useChatButton; diff --git a/src/modules/navigationIsland/CustomButtons.tsx b/src/modules/navigationIsland/CustomButtons.tsx index 73d073ad..f9d2540f 100644 --- a/src/modules/navigationIsland/CustomButtons.tsx +++ b/src/modules/navigationIsland/CustomButtons.tsx @@ -4,13 +4,13 @@ const baseStyle = (theme: Theme) => ({ // remove default button borders border: 'unset', // set padding for icon - padding: '12px', + padding: '8px', // transition smoothly between colors transition: 'all ease 100ms', // round the corners borderRadius: theme.spacing(1), - // set a fixed height 12 + 12 for padding + 24 for the icon height - height: '48px', + // set a fixed height 8 + 8 for padding + 24 for the icon height + height: '40px', '&:disabled svg': { color: 'gray', }, diff --git a/src/modules/navigationIsland/NavigationIsland.tsx b/src/modules/navigationIsland/NavigationIsland.tsx index 6fda1e6e..b9889e7b 100644 --- a/src/modules/navigationIsland/NavigationIsland.tsx +++ b/src/modules/navigationIsland/NavigationIsland.tsx @@ -4,17 +4,21 @@ import useChatButton from './ChatButton'; import usePinnedItemsButton from './PinnedItemsButton'; import usePreviousNextButtons from './PreviousNextButtons'; -const NavigationIslandBox = (): JSX.Element | null => { +const NavigationIslandBox = (): JSX.Element | false => { const { previousButton, nextButton } = usePreviousNextButtons(); const { chatButton } = useChatButton(); const { pinnedButton } = usePinnedItemsButton(); + // if all buttons are disabled do not show the island at all + if (!chatButton && !pinnedButton && !previousButton && !nextButton) { + return false; + } + return ( { // have some padding for the content that will be rendered inside p={1} > - + {previousButton && nextButton && ( - + {previousButton} {nextButton} )} - + {chatButton} {pinnedButton} diff --git a/src/modules/navigationIsland/PinnedItemsButton.tsx b/src/modules/navigationIsland/PinnedItemsButton.tsx index a1e74602..9e5e0a96 100644 --- a/src/modules/navigationIsland/PinnedItemsButton.tsx +++ b/src/modules/navigationIsland/PinnedItemsButton.tsx @@ -2,11 +2,7 @@ import { useParams } from 'react-router-dom'; import { Tooltip } from '@mui/material'; -import { - ItemTagType, - PermissionLevel, - PermissionLevelCompare, -} from '@graasp/sdk'; +import { PermissionLevel, PermissionLevelCompare } from '@graasp/sdk'; import { Pin, PinOff } from 'lucide-react'; @@ -21,26 +17,31 @@ import { ToolButton } from './CustomButtons'; const usePinnedItemsButton = (): { pinnedButton: JSX.Element | false } => { const { t } = usePlayerTranslation(); const { togglePinned, isPinnedOpen } = useLayoutContext(); - const { itemId } = useParams(); + const { rootId, itemId } = useParams(); const { data: item } = hooks.useItem(itemId); const { data: children } = hooks.useChildren(itemId, undefined, { enabled: !!item, }); - const { data: tags } = hooks.useItemsTags(children?.map(({ id }) => id)); - const pinnedCount = - children?.filter( - ({ id, settings: s }) => - s.isPinned && - // do not count hidden items as they are not displayed - !tags?.data?.[id].some(({ type }) => type === ItemTagType.Hidden), - )?.length || 0; + const { data: descendants } = hooks.useDescendants({ id: rootId }); + const childrenPinnedCount = + children?.filter(({ settings: s, hidden }) => s.isPinned && !hidden) + ?.length || 0; + const allPinnedCount = + descendants?.filter(({ settings: s, hidden }) => s.isPinned && !hidden) + ?.length || 0; + + // don't show the button if there are no items pinned in all descendants + if (allPinnedCount <= 0) { + return { pinnedButton: false }; + } + const canWrite = item?.permission ? PermissionLevelCompare.gte(item?.permission, PermissionLevel.Write) : false; // we should show the icon as open if there are pinned items and the drawer is open - const isOpen = isPinnedOpen && pinnedCount > 0; + const isOpen = isPinnedOpen && childrenPinnedCount > 0; - const isDisabled = pinnedCount <= 0; + const isDisabled = childrenPinnedCount <= 0; const tooltip = canWrite ? t(PLAYER.NAVIGATION_ISLAND_PINNED_BUTTON_HELPER_TEXT_WRITERS) : t(PLAYER.NAVIGATION_ISLAND_PINNED_BUTTON_HELPER_TEXT_READERS); diff --git a/src/modules/rightPanel/SideContent.tsx b/src/modules/rightPanel/SideContent.tsx index 3ae5b859..2ca0729a 100644 --- a/src/modules/rightPanel/SideContent.tsx +++ b/src/modules/rightPanel/SideContent.tsx @@ -6,12 +6,7 @@ import ExitFullscreenIcon from '@mui/icons-material/FullscreenExit'; import { Grid, Stack, Tooltip, styled } from '@mui/material'; import IconButton from '@mui/material/IconButton'; -import { - DiscriminatedItem, - ItemTagType, - ItemType, - getIdsFromPath, -} from '@graasp/sdk'; +import { DiscriminatedItem, ItemType, getIdsFromPath } from '@graasp/sdk'; import { useMobileView } from '@graasp/ui'; import { usePlayerTranslation } from '@/config/i18n'; @@ -71,7 +66,6 @@ const SideContent = ({ content, item }: Props): JSX.Element | null => { const { data: children } = hooks.useChildren(item.id, undefined, { enabled: !!item, }); - const { data: tags } = hooks.useItemsTags(children?.map(({ id }) => id)); const [searchParams] = useSearchParams(); const { @@ -106,12 +100,8 @@ const SideContent = ({ content, item }: Props): JSX.Element | null => { ); const pinnedCount = - children?.filter( - ({ id, settings: s }) => - s.isPinned && - // do not count hidden items as they are not displayed - !tags?.data?.[id]?.some(({ type }) => type === ItemTagType.Hidden), - )?.length || 0; + children?.filter(({ settings: s, hidden }) => s.isPinned && !hidden) + ?.length || 0; const toggleFullscreen = () => { setIsFullscreen(!isFullscreen); @@ -173,7 +163,7 @@ const SideContent = ({ content, item }: Props): JSX.Element | null => { open={isPinnedOpen} > {/* show parents pinned items */} - + {parentsIds.map((i) => ( ))} diff --git a/src/utils/item.ts b/src/utils/item.ts index c85e64cf..66617cc8 100644 --- a/src/utils/item.ts +++ b/src/utils/item.ts @@ -42,9 +42,6 @@ export const isHidden = ( return false; }; -export const stripHtml = (str?: string | null): string | undefined => - str?.replace(/<[^>]*>?/gm, ''); - export const paginationContentFilter = (items: PackedItem[]): PackedItem[] => items .filter((i) => i.type !== ItemType.FOLDER)