From bde9dd9f3b9cb8250121eeb7902430afb95aa10f Mon Sep 17 00:00:00 2001 From: Basile Spaenlehauer Date: Fri, 24 Mar 2023 10:44:18 +0100 Subject: [PATCH] feat(ui): show item status with badges in table and cards (#591) --- cypress/e2e/item/hide/hideItem.cy.js | 23 ++-- cypress/e2e/item/view/viewFolder.cy.js | 20 ++- cypress/support/commands.js | 3 + cypress/support/server.js | 23 ++++ package.json | 11 +- src/components/common/FavoriteButton.tsx | 2 +- src/components/common/HideButton.tsx | 6 +- .../main/{Item.tsx => ItemCard.tsx} | 27 ++-- src/components/main/Items.tsx | 7 +- src/components/main/ItemsGrid.tsx | 17 ++- src/components/main/ItemsTable.tsx | 46 +++++-- src/components/table/BadgesCellRenderer.tsx | 46 +++++++ src/config/constants.ts | 2 - src/config/selectors.ts | 2 + src/utils/item.ts | 13 +- src/utils/itemTag.js | 43 ------ src/utils/itemTag.ts | 85 ++++++++++++ yarn.lock | 125 ++++++++++-------- 18 files changed, 345 insertions(+), 156 deletions(-) rename src/components/main/{Item.tsx => ItemCard.tsx} (82%) create mode 100644 src/components/table/BadgesCellRenderer.tsx delete mode 100644 src/utils/itemTag.js create mode 100644 src/utils/itemTag.ts diff --git a/cypress/e2e/item/hide/hideItem.cy.js b/cypress/e2e/item/hide/hideItem.cy.js index df4e057e9..39f190249 100644 --- a/cypress/e2e/item/hide/hideItem.cy.js +++ b/cypress/e2e/item/hide/hideItem.cy.js @@ -1,6 +1,7 @@ import { HOME_PATH, buildItemPath } from '../../../../src/config/paths'; import { HIDDEN_ITEM_BUTTON_CLASS, + buildHideButtonId, buildItemMenu, buildItemMenuButtonId, } from '../../../../src/config/selectors'; @@ -10,13 +11,15 @@ import { HIDDEN_ITEM, ITEMS_SETTINGS, } from '../../../fixtures/items'; -import { TABLE_ITEM_RENDER_TIME } from '../../../support/constants'; -const toggleHideButton = (itemId) => { - cy.wait(TABLE_ITEM_RENDER_TIME); +const toggleHideButton = (itemId, isHidden = false) => { const menuSelector = `#${buildItemMenuButtonId(itemId)}`; cy.get(menuSelector).click(); - cy.get(`#${buildItemMenu(itemId)} .${HIDDEN_ITEM_BUTTON_CLASS}`).click(); + + cy.wait('@getItemTags'); + cy.get(`#${buildItemMenu(itemId)} .${HIDDEN_ITEM_BUTTON_CLASS}`) + .should('have.attr', 'data-cy', buildHideButtonId(isHidden)) + .click(); }; const HIDDEN_ITEM_TAG_ID = Cypress.env('HIDDEN_ITEM_TAG_ID'); @@ -31,7 +34,7 @@ describe('Hiding Item', () => { cy.visit(HOME_PATH); const item = ITEMS_SETTINGS.items[1]; - toggleHideButton(item.id); + toggleHideButton(item.id, false); cy.wait(`@postItemTag`).then( ({ @@ -48,8 +51,8 @@ describe('Hiding Item', () => { cy.visit(HOME_PATH); const item = HIDDEN_ITEM; - cy.wait(5000); - toggleHideButton(item.id); + // make sure to wait for the tags to be fetched + toggleHideButton(item.id, true); cy.wait('@deleteItemTag').then(({ request: { url } }) => { expect(url).to.contain(item.tags[1].id); @@ -58,7 +61,6 @@ describe('Hiding Item', () => { it('Cannot hide child of hidden item', () => { cy.visit(buildItemPath(HIDDEN_ITEM.id)); - cy.wait(TABLE_ITEM_RENDER_TIME); cy.get(`#${buildItemMenuButtonId(CHILD_HIDDEN_ITEM.id)}`).click(); cy.get( `#${buildItemMenu(CHILD_HIDDEN_ITEM.id)} .${HIDDEN_ITEM_BUTTON_CLASS}`, @@ -80,7 +82,7 @@ describe('Hiding Item', () => { cy.switchMode(ITEM_LAYOUT_MODES.GRID); const item = ITEMS_SETTINGS.items[1]; - toggleHideButton(item.id); + toggleHideButton(item.id, false); cy.wait(`@postItemTag`).then( ({ @@ -98,7 +100,7 @@ describe('Hiding Item', () => { cy.switchMode(ITEM_LAYOUT_MODES.GRID); const item = ITEMS_SETTINGS.items[0]; - toggleHideButton(item.id); + toggleHideButton(item.id, true); cy.wait('@deleteItemTag').then(({ request: { url } }) => { expect(url).to.contain(item.tags[1].id); @@ -109,7 +111,6 @@ describe('Hiding Item', () => { cy.visit(buildItemPath(HIDDEN_ITEM.id)); cy.switchMode(ITEM_LAYOUT_MODES.GRID); - cy.wait(TABLE_ITEM_RENDER_TIME); cy.get(`#${buildItemMenuButtonId(CHILD_HIDDEN_ITEM.id)}`).click(); cy.get( `#${buildItemMenu(CHILD_HIDDEN_ITEM.id)} .${HIDDEN_ITEM_BUTTON_CLASS}`, diff --git a/cypress/e2e/item/view/viewFolder.cy.js b/cypress/e2e/item/view/viewFolder.cy.js index 721d25985..8a351d119 100644 --- a/cypress/e2e/item/view/viewFolder.cy.js +++ b/cypress/e2e/item/view/viewFolder.cy.js @@ -27,7 +27,10 @@ import { SAMPLE_ITEMS, generateOwnItems } from '../../../fixtures/items'; import { GRAASP_LINK_ITEM } from '../../../fixtures/links'; import { CURRENT_USER } from '../../../fixtures/members'; import { SHARED_ITEMS } from '../../../fixtures/sharedItems'; -import { NAVIGATION_LOAD_PAUSE } from '../../../support/constants'; +import { + NAVIGATION_LOAD_PAUSE, + TABLE_ITEM_RENDER_TIME, +} from '../../../support/constants'; import { expectFolderViewScreenLayout } from '../../../support/viewUtils'; const translateBuilder = (key) => i18n.t(key, { ns: namespaces.builder }); @@ -53,6 +56,7 @@ describe('View Folder', () => { // should get own items cy.wait('@getOwnItems').then(({ response: { body } }) => { + cy.wait('@getItemMemberships'); // check item is created and displayed for (const item of body) { cy.get(`#${buildItemCard(item.id)}`).should('exist'); @@ -64,12 +68,14 @@ describe('View Folder', () => { cy.goToItemInGrid(childId); // should get children - cy.wait('@getChildren').then(({ response: { body } }) => { - // check item is created and displayed - for (const item of body) { - cy.get(`#${buildItemCard(item.id)}`).should('exist'); - } - }); + cy.wait('@getChildren', { timeout: TABLE_ITEM_RENDER_TIME }).then( + ({ response: { body } }) => { + // check item is created and displayed + for (const item of body) { + cy.get(`#${buildItemCard(item.id)}`).should('exist'); + } + }, + ); // root title cy.get(`#${NAVIGATION_ROOT_ID}`).should( diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 16126d175..40cfddc1c 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -65,6 +65,7 @@ import { mockGetItemValidationReviewStatuses, mockGetItemValidationStatuses, mockGetItems, + mockGetItemsTags, mockGetMember, mockGetMemberMentions, mockGetMembers, @@ -234,6 +235,8 @@ Cypress.Commands.add( mockGetItemTags(items); + mockGetItemsTags(items); + mockPostItemTag(items, postItemTagError); mockDeleteItemTag(deleteItemTagError); diff --git a/cypress/support/server.js b/cypress/support/server.js index 3d28b18e3..2d275e7cc 100644 --- a/cypress/support/server.js +++ b/cypress/support/server.js @@ -66,6 +66,7 @@ const { buildGetItemMembershipsForItemsRoute, buildGetPublicItemMembershipsForItemsRoute, buildGetItemTagsRoute, + buildGetItemsTagsRoute, GET_TAGS_ROUTE, buildPutItemLoginSchema, buildPostItemTagRoute, @@ -1041,6 +1042,28 @@ export const mockGetItemTags = (items) => { ).as('getItemTags'); }; +export const mockGetItemsTags = (items) => { + cy.intercept( + { + method: DEFAULT_GET.method, + url: `${API_HOST}/items/tags?id=*`, + }, + ({ reply, url }) => { + let { id: ids } = qs.parse(url.split('?')[1]); + if (typeof ids === 'string') { + ids = [ids]; + } + const result = ids?.map( + (itemId) => + items.find(({ id }) => id === itemId)?.tags || [ + { statusCode: StatusCodes.NOT_FOUND }, + ], + ); + reply(result); + }, + ).as('getItemsTags'); +}; + export const mockGetTags = (tags) => { cy.intercept( { diff --git a/package.json b/package.json index 9eed9e019..2cd2a15cc 100644 --- a/package.json +++ b/package.json @@ -14,14 +14,14 @@ "@emotion/styled": "11.10.5", "@graasp/chatbox": "1.1.0", "@graasp/query-client": "0.4.0", - "@graasp/sdk": "0.9.2", - "@graasp/translations": "1.8.0", + "@graasp/sdk": "0.9.3", + "@graasp/translations": "1.10.0", "@graasp/ui": "2.2.0", "@mui/icons-material": "5.11.0", "@mui/lab": "5.0.0-alpha.117", "@mui/material": "5.11.6", - "@sentry/react": "7.39.0", - "@sentry/tracing": "7.39.0", + "@sentry/react": "7.44.2", + "@sentry/tracing": "7.44.2", "@uppy/core": "3.0.4", "@uppy/dashboard": "3.2.0", "@uppy/drag-drop": "3.0.1", @@ -77,7 +77,7 @@ "check": "yarn prettier:check && yarn lint && yarn type-check", "prettier:check": "prettier --check src/**/*.{js,ts,tsx,json}", "prettier:write": "prettier --write src/**/*.{js,ts,tsx,json}", - "cypress:open": "env-cmd -f ./.env.test cypress open", + "cypress:open": "env-cmd -f ./.env.local cypress open", "cypress": "concurrently \"yarn start:test\" \"wait-on http://localhost:3111 && yarn cypress:run\"", "cypress:run": "env-cmd -f ./.env.test cypress run --headless --browser chrome --spec \"cypress/**/*.cy.js\"", "postinstall": "husky install", @@ -147,6 +147,7 @@ "@mui/icons-material": "5.11.0", "@mui/material": "5.11.6", "immer": "9.0.6", + "@graasp/sdk": "0.9.3", "immutable": "4.2.4", "@svgr/webpack": "6.5.1" }, diff --git a/src/components/common/FavoriteButton.tsx b/src/components/common/FavoriteButton.tsx index b9d1610ba..728b82e4c 100644 --- a/src/components/common/FavoriteButton.tsx +++ b/src/components/common/FavoriteButton.tsx @@ -64,7 +64,7 @@ const FavoriteButton: FC = ({ item, size, type, onClick }) => { return ( {icon} {text} diff --git a/src/components/main/Item.tsx b/src/components/main/ItemCard.tsx similarity index 82% rename from src/components/main/Item.tsx rename to src/components/main/ItemCard.tsx index 399845428..d370a23ef 100644 --- a/src/components/main/Item.tsx +++ b/src/components/main/ItemCard.tsx @@ -5,7 +5,11 @@ import { CSSProperties, FC, PropsWithChildren } from 'react'; import { Link } from 'react-router-dom'; import { DiscriminatedItem, ItemType, getEmbeddedLinkExtra } from '@graasp/sdk'; -import { ItemMembershipRecord, ItemRecord } from '@graasp/sdk/frontend'; +import { + ItemMembershipRecord, + ItemRecord, + TagRecord, +} from '@graasp/sdk/frontend'; import { Card as GraaspCard, Thumbnail } from '@graasp/ui'; import { DESCRIPTION_MAX_LENGTH } from '../../config/constants'; @@ -18,6 +22,7 @@ import { isItemUpdateAllowedForUser } from '../../utils/membership'; import EditButton from '../common/EditButton'; import FavoriteButton from '../common/FavoriteButton'; import { useCurrentUserContext } from '../context/CurrentUserContext'; +import BadgesCellRenderer from '../table/BadgesCellRenderer'; import DownloadButton from './DownloadButton'; import ItemMenu from './ItemMenu'; @@ -39,12 +44,11 @@ const NameWrapper = ({ type Props = { item: ItemRecord; memberships: List; + tagList?: List; }; -const ItemComponent: FC = ({ item, memberships }) => { - const { id, name, description } = item; - - const alt = name; +const ItemComponent: FC = ({ item, memberships, tagList }) => { + const alt = item.name; const defaultValueComponent = ( = ({ item, memberships }) => { item.toJS() as DiscriminatedItem } /> - + )} ); + // here we use the same component as the table this is why it is instantiated a bit weirdly + const Badges = BadgesCellRenderer({ tagList }); return ( } + name={item.name} creator={member?.name} ItemMenu={ = ({ item, memberships }) => { /> } Thumbnail={ThumbnailComponent} - cardId={buildItemCard(id)} + cardId={buildItemCard(item.id)} NameWrapper={NameWrapper({ - id, + id: item.id, style: { textDecoration: 'none', color: 'inherit', diff --git a/src/components/main/Items.tsx b/src/components/main/Items.tsx index 07d7a035c..615dabd9f 100644 --- a/src/components/main/Items.tsx +++ b/src/components/main/Items.tsx @@ -10,7 +10,7 @@ import { useItemSearch } from '../item/ItemSearch'; import ItemsGrid from './ItemsGrid'; import ItemsTable from './ItemsTable'; -const { useManyItemMemberships } = hooks; +const { useManyItemMemberships, useTags } = hooks; type Props = { id: string; @@ -56,12 +56,13 @@ const Items = ({ ? itemSearch?.results?.map(({ id: itemId }) => itemId).toArray() : [], ); + const { data: tagList, isLoading: isLoadingTagList } = useTags(); // todo: disable depending on showCreator const { data: creators } = hooks.useMembers( Object.keys(items?.groupBy(({ creator }) => creator)?.toJS() ?? []), ); - if (isMembershipsLoading) { + if (isMembershipsLoading || isLoadingTagList) { return ; } @@ -73,6 +74,7 @@ const Items = ({ title={title} items={itemSearch.results} manyMemberships={manyMemberships} + tagList={tagList} // This enables the possiblity to display messages (item is empty, no search result) itemSearch={itemSearch} headerElements={[itemSearch.input, ...headerElements]} @@ -89,6 +91,7 @@ const Items = ({ defaultSortedColumn={defaultSortedColumn} items={itemSearch.results} manyMemberships={manyMemberships} + tagList={tagList} headerElements={[itemSearch.input, ...headerElements]} isSearching={Boolean(itemSearch.text)} ToolbarActions={ToolbarActions} diff --git a/src/components/main/ItemsGrid.tsx b/src/components/main/ItemsGrid.tsx index 8354870b6..e44b7a6e4 100644 --- a/src/components/main/ItemsGrid.tsx +++ b/src/components/main/ItemsGrid.tsx @@ -8,7 +8,11 @@ import Select from '@mui/material/Select'; import { useState } from 'react'; -import { ItemMembershipRecord, ItemRecord } from '@graasp/sdk/frontend'; +import { + ItemMembershipRecord, + ItemRecord, + TagRecord, +} from '@graasp/sdk/frontend'; import { BUILDER } from '@graasp/translations'; import { GRID_ITEMS_PER_PAGE_CHOICES } from '../../config/constants'; @@ -22,7 +26,7 @@ import { getMembershipsForItem } from '../../utils/membership'; import FolderDescription from '../item/FolderDescription'; import { NoItemSearchResult } from '../item/ItemSearch'; import EmptyItem from './EmptyItem'; -import Item from './Item'; +import ItemCard from './ItemCard'; import ItemsToolbar from './ItemsToolbar'; const StyledBox = styled(Box)(({ theme }) => ({ @@ -34,7 +38,8 @@ const StyledBox = styled(Box)(({ theme }) => ({ type Props = { id?: string; items?: List; - manyMemberships: List>; + manyMemberships?: List>; + tagList?: List; title: string; itemSearch?: { text: string; @@ -51,7 +56,8 @@ const ItemsGrid = ({ title, itemSearch, headerElements = [], - manyMemberships, + manyMemberships = List(), + tagList = List(), isEditing = false, parentId, }: Props): JSX.Element => { @@ -87,13 +93,14 @@ const ItemsGrid = ({ return itemsInPage.map((item) => ( - )); diff --git a/src/components/main/ItemsTable.tsx b/src/components/main/ItemsTable.tsx index af769b4af..992e69d9e 100644 --- a/src/components/main/ItemsTable.tsx +++ b/src/components/main/ItemsTable.tsx @@ -16,6 +16,7 @@ import { ItemMembershipRecord, ItemRecord, MemberRecord, + TagRecord, } from '@graasp/sdk/frontend'; import { BUILDER, COMMON } from '@graasp/translations'; import { Table as GraaspTable } from '@graasp/ui/dist/table'; @@ -33,6 +34,7 @@ import { formatDate } from '../../utils/date'; import { useCurrentUserContext } from '../context/CurrentUserContext'; import FolderDescription from '../item/FolderDescription'; import ActionsCellRenderer from '../table/ActionsCellRenderer'; +import BadgesCellRenderer from '../table/BadgesCellRenderer'; import NameCellRenderer from '../table/ItemNameCellRenderer'; import MemberNameCellRenderer from '../table/MemberNameCellRenderer'; import ItemsToolbar from './ItemsToolbar'; @@ -42,7 +44,8 @@ const { useItem } = hooks; type Props = { id?: string; items: List; - manyMemberships: List>; + manyMemberships?: List>; + tagList?: List; tableTitle: string; headerElements?: JSX.Element[]; isSearching?: boolean; @@ -66,6 +69,7 @@ const ItemsTable: FC = ({ id: tableId = '', items: rows = List(), manyMemberships = List(), + tagList = List(), headerElements = [], isSearching = false, actions, @@ -167,34 +171,55 @@ const ItemsTable: FC = ({ member, }); + const BadgesComponent = BadgesCellRenderer({ + tagList, + }); + // never changes, so we can use useMemo const columnDefs = useMemo(() => { const columns: ColDef[] = [ { + field: 'name', + headerName: translateBuilder(BUILDER.ITEMS_TABLE_NAME_HEADER), headerCheckboxSelection: true, checkboxSelection: true, - headerName: translateBuilder(BUILDER.ITEMS_TABLE_NAME_HEADER), cellRenderer: NameCellRenderer(showThumbnails), flex: 4, comparator: GraaspTable.textComparator, sort: defaultSortedColumn?.name, - field: 'name', tooltipField: 'name', }, + { + // todo: add translation of of header + // headerName: translateBuilder(BUILDER.ITEMS_TABLE_STATUS_HEADER), + cellRenderer: BadgesComponent, + type: 'rightAligned', + flex: 1, + suppressAutoSize: true, + maxWidth: 100, + cellStyle: { + display: 'flex', + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + }, + }, { field: 'type', headerName: translateBuilder(BUILDER.ITEMS_TABLE_TYPE_HEADER), type: 'rightAligned', cellRenderer: ({ data }: { data: DiscriminatedItem }) => translateEnums(data.type), - flex: 2, + minWidth: 90, + maxWidth: 120, comparator: GraaspTable.textComparator, sort: defaultSortedColumn?.type, }, { field: 'updatedAt', headerName: translateBuilder(BUILDER.ITEMS_TABLE_UPDATED_AT_HEADER), - flex: 2, + maxWidth: 160, + minWidth: 80, type: 'rightAligned', valueFormatter: dateColumnFormatter, comparator: GraaspTable.dateComparator, @@ -207,22 +232,21 @@ const ItemsTable: FC = ({ headerName: translateBuilder(BUILDER.ITEMS_TABLE_ACTIONS_HEADER), colId: 'actions', type: 'rightAligned', - flex: 3, cellStyle: { paddingLeft: '0!important', paddingRight: '0!important', textAlign: 'right', }, sortable: false, + suppressAutoSize: true, // prevent ellipsis for small screens - minWidth: 165, + minWidth: 140, }, ]; if (showCreator) { - columns.splice(1, 0, { + columns.splice(2, 0, { field: 'creator', - flex: 3, headerName: translateBuilder(BUILDER.ITEMS_TABLE_CREATOR_HEADER), colId: 'creator', type: 'rightAligned', @@ -245,6 +269,7 @@ const ItemsTable: FC = ({ translateBuilder, defaultSortedColumn, ActionComponent, + BadgesComponent, actions, showThumbnails, ]); @@ -265,6 +290,9 @@ const ItemsTable: FC = ({ rowData={rows.toJS() as DiscriminatedItem[]} emptyMessage={translateBuilder(BUILDER.ITEMS_TABLE_EMPTY_MESSAGE)} onDragEnd={onDragEnd} + // todo: use DiscriminatedItem in ui + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore onCellClicked={onCellClicked} getRowId={getRowNodeId} isClickable={clickable} diff --git a/src/components/table/BadgesCellRenderer.tsx b/src/components/table/BadgesCellRenderer.tsx new file mode 100644 index 000000000..7b183feee --- /dev/null +++ b/src/components/table/BadgesCellRenderer.tsx @@ -0,0 +1,46 @@ +import { List } from 'immutable'; + +import { Item } from '@graasp/sdk'; +import { ItemRecord, TagRecord } from '@graasp/sdk/frontend'; +import { ItemBadges } from '@graasp/ui'; + +import { hooks } from '../../config/queryClient'; +import { + isItemHidden, + isItemPublic, + isItemPublished, +} from '../../utils/itemTag'; + +type Props = { + tagList: List; +}; + +type ChildCompProps = { + data: Item | ItemRecord; +}; + +// items and memberships match by index +const BadgesCellRenderer = ({ + tagList, +}: Props): ((arg: ChildCompProps) => JSX.Element) => { + const ChildComponent = ({ data: item }: ChildCompProps) => { + const { data: itemTags } = hooks.useItemTags(item.id); + + const isHidden = isItemHidden({ tags: tagList, itemTags }); + const isPublic = isItemPublic({ tags: tagList, itemTags }); + const isPublished = isItemPublished({ tags: tagList, itemTags }); + const isPinned = Boolean(item?.settings?.isPinned); + + return ( + + ); + }; + return ChildComponent; +}; + +export default BadgesCellRenderer; diff --git a/src/config/constants.ts b/src/config/constants.ts index 4e241da05..a9e54944c 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -170,8 +170,6 @@ export const GRAASP_LOGO_HEADER_HEIGHT = 40; export const ITEMS_TABLE_CONTAINER_HEIGHT = '65vh'; -export const DRAG_ICON_SIZE = 18; - export const THUMBNAIL_ASPECT = 1; export const THUMBNAIL_EXTENSION = 'image/jpeg'; export const THUMBNAIL_SETTING_MAX_WIDTH = 200; diff --git a/src/config/selectors.ts b/src/config/selectors.ts index 9cb1a4519..1465e1945 100644 --- a/src/config/selectors.ts +++ b/src/config/selectors.ts @@ -29,6 +29,8 @@ export const FAVORITE_ITEM_BUTTON_CLASS = 'favoriteButton'; export const PIN_ITEM_BUTTON_CLASS = 'pinButton'; export const COLLAPSE_ITEM_BUTTON_CLASS = 'collapseButton'; export const HIDDEN_ITEM_BUTTON_CLASS = 'hideButton'; +export const buildHideButtonId = (hidden: boolean): string => + `hideButton-${hidden ? 'hidden' : 'visible'}`; export const SHARE_ITEM_BUTTON_CLASS = 'itemMenuShareButton'; export const PUBLISH_ITEM_BUTTON_CLASS = 'publishItemButton'; export const RESTORE_ITEMS_BUTTON_CLASS = 'itemMenuRestoreButton'; diff --git a/src/utils/item.ts b/src/utils/item.ts index cf4d1b787..7d34a66ed 100644 --- a/src/utils/item.ts +++ b/src/utils/item.ts @@ -101,25 +101,24 @@ export const isItemValid = (item: Partial): boolean => { return false; } - const { name, type: itemType, extra } = item; - const shouldHaveName = Boolean(name); + const shouldHaveName = Boolean(item.name); // item should have a type let hasValidTypeProperties = - itemType && Object.values(ItemType).includes(itemType); - switch (itemType) { + item.type && Object.values(ItemType).includes(item.type); + switch (item.type) { case ItemType.LINK: { - const { url } = getEmbeddedLinkExtra(extra) || {}; + const { url } = getEmbeddedLinkExtra(item.extra) || {}; hasValidTypeProperties = isUrlValid(url); break; } case ItemType.APP: { - const { url } = getAppExtra(extra) || {}; + const { url } = getAppExtra(item.extra) || {}; hasValidTypeProperties = isUrlValid(url); break; } case ItemType.DOCUMENT: { - const { content } = getDocumentExtra(extra) || {}; + const { content } = getDocumentExtra(item.extra) || {}; hasValidTypeProperties = content?.length > 0; break; } diff --git a/src/utils/itemTag.js b/src/utils/itemTag.js deleted file mode 100644 index 308e4670b..000000000 --- a/src/utils/itemTag.js +++ /dev/null @@ -1,43 +0,0 @@ -import { SETTINGS } from '../config/constants'; - -export const getItemLoginTag = (tags) => - tags?.find(({ name }) => name === SETTINGS.ITEM_LOGIN.name); - -export const getTagByName = (tags, name) => - tags?.find(({ name: thisName }) => thisName === name); - -export const hasItemLoginEnabled = ({ tags, itemTags }) => - Boolean(itemTags?.find(({ tagId }) => tagId === getItemLoginTag(tags)?.id)); - -export const getVisibilityTagAndItemTag = ({ tags, itemTags }) => { - const tagIds = [ - getTagByName(tags, SETTINGS.ITEM_PUBLIC.name)?.id, - getTagByName(tags, SETTINGS.ITEM_LOGIN.name)?.id, - ]; - const visibilityItemTags = itemTags?.filter(({ tagId }) => - tagIds.includes(tagId), - ); - const sorted = visibilityItemTags.sort( - (a, b) => tagIds.indexOf(a.tagId) - tagIds.indexOf(b.tagId), - ); - const bestVisibilityItemTag = sorted?.get(0); - const visibilityTagValue = tags.find( - ({ id }) => id === bestVisibilityItemTag?.tagId, - ); - // return best visibility tag - return { itemTag: bestVisibilityItemTag, tag: visibilityTagValue }; -}; - -export const getItemPublicTag = (tags) => - tags?.find(({ name }) => name === SETTINGS.ITEM_PUBLIC.name); - -export const isItemPublic = ({ tags, itemTags }) => - Boolean(itemTags?.find(({ tagId }) => tagId === getItemPublicTag(tags)?.id)); - -export const getItemPublishedTag = (tags) => - tags?.find(({ name }) => name === SETTINGS.ITEM_PUBLISHED.name); - -export const isItemPublished = ({ tags, itemTags }) => - Boolean( - itemTags?.find(({ tagId }) => tagId === getItemPublishedTag(tags)?.id), - ); diff --git a/src/utils/itemTag.ts b/src/utils/itemTag.ts new file mode 100644 index 000000000..4a23097ee --- /dev/null +++ b/src/utils/itemTag.ts @@ -0,0 +1,85 @@ +import { List } from 'immutable'; + +import { ItemTagRecord, TagRecord } from '@graasp/sdk/frontend'; + +import { SETTINGS } from '../config/constants'; + +export const getItemLoginTag = (tags: List): TagRecord => + tags?.find(({ name }) => name === SETTINGS.ITEM_LOGIN.name); + +export const getTagByName = (tags: List, name: string): TagRecord => + tags?.find(({ name: thisName }) => thisName === name); + +export const hasItemLoginEnabled = ({ + tags, + itemTags, +}: { + tags: List; + itemTags: List; +}): boolean => + Boolean(itemTags?.find(({ tagId }) => tagId === getItemLoginTag(tags)?.id)); + +export const getVisibilityTagAndItemTag = ({ + tags, + itemTags, +}: { + tags: List; + itemTags: List; +}): { + tag: TagRecord; + itemTag: ItemTagRecord; +} => { + const tagIds = [ + getTagByName(tags, SETTINGS.ITEM_PUBLIC.name)?.id, + getTagByName(tags, SETTINGS.ITEM_LOGIN.name)?.id, + ]; + const visibilityItemTags = itemTags?.filter(({ tagId }) => + tagIds.includes(tagId), + ); + const sorted = visibilityItemTags.sort( + (a, b) => tagIds.indexOf(a.tagId) - tagIds.indexOf(b.tagId), + ); + const bestVisibilityItemTag = sorted?.get(0); + const visibilityTagValue = tags.find( + ({ id }) => id === bestVisibilityItemTag?.tagId, + ); + // return best visibility tag + return { itemTag: bestVisibilityItemTag, tag: visibilityTagValue }; +}; + +export const isItemHidden = ({ + tags, + itemTags, +}: { + tags: List; + itemTags: List; +}): boolean => + Boolean( + itemTags?.find(({ tagId }) => tagId === getTagByName(tags, 'hidden')?.id), + ); + +export const getItemPublicTag = (tags: List): TagRecord => + tags?.find(({ name }) => name === SETTINGS.ITEM_PUBLIC.name); + +export const isItemPublic = ({ + tags, + itemTags, +}: { + tags: List; + itemTags: List; +}): boolean => + Boolean(itemTags?.find(({ tagId }) => tagId === getItemPublicTag(tags)?.id)); + +export const getItemPublishedTag = (tags: List): TagRecord => + tags?.find(({ name }) => name === SETTINGS.ITEM_PUBLISHED.name); + +export const isItemPublished = ({ + tags, + itemTags, +}: { + tags: List; + itemTags: List; +}): boolean => + Boolean( + itemTags?.find(({ tagId }) => tagId === getItemPublishedTag(tags)?.id), + ); diff --git a/yarn.lock b/yarn.lock index 0c038c39c..7aac22c3e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2393,9 +2393,9 @@ __metadata: languageName: node linkType: hard -"@graasp/sdk@npm:0.9.2": - version: 0.9.2 - resolution: "@graasp/sdk@npm:0.9.2" +"@graasp/sdk@npm:0.9.3": + version: 0.9.3 + resolution: "@graasp/sdk@npm:0.9.3" dependencies: "@fastify/secure-session": 5.3.0 aws-sdk: 2.1310.0 @@ -2406,7 +2406,16 @@ __metadata: qs: 6.11.0 slonik: 28.1.1 uuid: 9.0.0 - checksum: 49daf48b8877b1a0bdd0612c5a88be22860de4e94c494978732add26d38b5d0a6784df8eb8caedd7972b2767c78258c056c89889c73eeeb3971f2ff989a7f1e0 + checksum: 222e8d7e00f5c4fbfb23d596899edd0afcaa0640c540ae90f85d6cd6f92b1bb7832bf50504fecd4aafe1a185b79ecae42a75163bd5fca5975e535fbeb84c3049 + languageName: node + linkType: hard + +"@graasp/translations@npm:1.10.0": + version: 1.10.0 + resolution: "@graasp/translations@npm:1.10.0" + dependencies: + i18next: 21.8.1 + checksum: 96c5b1f7612082846c92ef46dbd6392a2233a62a1ec4d987cc66b689af51a3f30707545501ab49f282687adbb52d81919711e0eee2cac8566571059a04638093 languageName: node linkType: hard @@ -3284,82 +3293,92 @@ __metadata: languageName: node linkType: hard -"@sentry/browser@npm:7.39.0": - version: 7.39.0 - resolution: "@sentry/browser@npm:7.39.0" +"@sentry-internal/tracing@npm:7.44.2": + version: 7.44.2 + resolution: "@sentry-internal/tracing@npm:7.44.2" dependencies: - "@sentry/core": 7.39.0 - "@sentry/replay": 7.39.0 - "@sentry/types": 7.39.0 - "@sentry/utils": 7.39.0 + "@sentry/core": 7.44.2 + "@sentry/types": 7.44.2 + "@sentry/utils": 7.44.2 tslib: ^1.9.3 - checksum: e4b83ae3640df6d22774e8d02c65c51bf747e810285c8d39b4d5eead47b25b32736cd3b3f68d20145c023b11391c66105204620e26acdfb79e216363b4d74fae + checksum: f8e3a3fa2b7262859f2d677eb46230dfe58167f9fa1f651994e1532a5b4e435c4e6ea378f0d9ea18c2eeb5c4bded158bf5cb6865649ad2be884b36d14b41753f languageName: node linkType: hard -"@sentry/core@npm:7.39.0": - version: 7.39.0 - resolution: "@sentry/core@npm:7.39.0" +"@sentry/browser@npm:7.44.2": + version: 7.44.2 + resolution: "@sentry/browser@npm:7.44.2" dependencies: - "@sentry/types": 7.39.0 - "@sentry/utils": 7.39.0 + "@sentry-internal/tracing": 7.44.2 + "@sentry/core": 7.44.2 + "@sentry/replay": 7.44.2 + "@sentry/types": 7.44.2 + "@sentry/utils": 7.44.2 tslib: ^1.9.3 - checksum: b00095a339fac77d17932e097d78c1e701cda63751e0a13f7be2f4cbdcf28ce6dcefd46f8c1e9ee85b15361b8884ca7d27d7e34cebae45b514f678e3ac49994c + checksum: d6e4e8f013ce2bfada2670cfe09285d3b1060cdea158fa700f6384a4dd5e3cd8f7d4210e2e541c2a44eec366d573d8b5c0d0e655dab9effeb21ca8667701bec4 languageName: node linkType: hard -"@sentry/react@npm:7.39.0": - version: 7.39.0 - resolution: "@sentry/react@npm:7.39.0" +"@sentry/core@npm:7.44.2": + version: 7.44.2 + resolution: "@sentry/core@npm:7.44.2" dependencies: - "@sentry/browser": 7.39.0 - "@sentry/types": 7.39.0 - "@sentry/utils": 7.39.0 + "@sentry/types": 7.44.2 + "@sentry/utils": 7.44.2 + tslib: ^1.9.3 + checksum: 2efcb9a09d0419ce7960c154d31dd017640dee203a7cf5681c0cd31b9ce116e34e05e20096d24bf5d2ff827afa016c02c287bf3808c900cec9e4906d6a8a57a5 + languageName: node + linkType: hard + +"@sentry/react@npm:7.44.2": + version: 7.44.2 + resolution: "@sentry/react@npm:7.44.2" + dependencies: + "@sentry/browser": 7.44.2 + "@sentry/types": 7.44.2 + "@sentry/utils": 7.44.2 hoist-non-react-statics: ^3.3.2 tslib: ^1.9.3 peerDependencies: react: 15.x || 16.x || 17.x || 18.x - checksum: b2ff10027c86199081f82659e805d49fd0ecd787b5b8b49c6a2a429e27d29eee4c4dedd2f67c71cae1384202561d20276943c3131e01eec15b7b07c61c9afae3 + checksum: b03f4cdb08ec6b6fce8646de782745bbaf8512b05cf5a8bfdb5b09ce781428899c475cfe023d958afbf9e844c0a552caac2b00d036cc06e564ad8eb911bb5817 languageName: node linkType: hard -"@sentry/replay@npm:7.39.0": - version: 7.39.0 - resolution: "@sentry/replay@npm:7.39.0" +"@sentry/replay@npm:7.44.2": + version: 7.44.2 + resolution: "@sentry/replay@npm:7.44.2" dependencies: - "@sentry/core": 7.39.0 - "@sentry/types": 7.39.0 - "@sentry/utils": 7.39.0 - checksum: f522147680e3e805d6d1ccb2317646f45abb662878405905b5bc270b78ac82f26d9708b0e53bce741c30146cd250b3e85743b957621721d5baae1774d3f6edf6 + "@sentry/core": 7.44.2 + "@sentry/types": 7.44.2 + "@sentry/utils": 7.44.2 + checksum: 2159043af713c45c03e6cd07ec3e262dbf7357edda6a0ef6bda8f8e1e40484af767f73e71cd1d4cbed6cf39f481040ff7867bc2ec9db33390bc0407827852dc1 languageName: node linkType: hard -"@sentry/tracing@npm:7.39.0": - version: 7.39.0 - resolution: "@sentry/tracing@npm:7.39.0" +"@sentry/tracing@npm:7.44.2": + version: 7.44.2 + resolution: "@sentry/tracing@npm:7.44.2" dependencies: - "@sentry/core": 7.39.0 - "@sentry/types": 7.39.0 - "@sentry/utils": 7.39.0 - tslib: ^1.9.3 - checksum: ee9945ddfbbcc53feffed72510acbbf176bf0cd3c36bb5cd75179423ffb0d2700e4d367abf8b57fe289dc0f19939e52b9c4d4bf91492905c1413571bd1f3b73e + "@sentry-internal/tracing": 7.44.2 + checksum: 19465abd4f0ec914d9b1c865de65a38df495a80d2994613643069785d4a1a1850882da1432b40df5d088a889421eb828a386c3693490d4905e3e8f5a98eab64c languageName: node linkType: hard -"@sentry/types@npm:7.39.0": - version: 7.39.0 - resolution: "@sentry/types@npm:7.39.0" - checksum: 7d8b3f5fdb033240750bfdd4398b596b24bb6963d1e923b9fe6c5226b19dd965e53cec9790bbf13340641e0006b8f3d0958613a8561b42ae3ff22da1c419df27 +"@sentry/types@npm:7.44.2": + version: 7.44.2 + resolution: "@sentry/types@npm:7.44.2" + checksum: 33c14f4089b1554f42d8f31dd9b9ae0d1960d470ce1f2d5b49899125b3dd20814eb4ec96418e93dc0471856bbe2a7cfac29bcc4a5ecd63a6e0f3ac53a929949a languageName: node linkType: hard -"@sentry/utils@npm:7.39.0": - version: 7.39.0 - resolution: "@sentry/utils@npm:7.39.0" +"@sentry/utils@npm:7.44.2": + version: 7.44.2 + resolution: "@sentry/utils@npm:7.44.2" dependencies: - "@sentry/types": 7.39.0 + "@sentry/types": 7.44.2 tslib: ^1.9.3 - checksum: 620c41f9fca3efb48e137f6d9635a0c11bd47262c68997dba13ca78296faaf0c60dc0e5c2d3f548d9eae3d57cc7ed165bd870de0e9ca3c9ce97ffc705a217fca + checksum: 726b0900e9e8d0aaf01adc5b6684e1ab4d07e63329ccac932b8fffd27209c56c3cd16b03f1290b4c57ed4fbb52aa4ca3d91f1c8c9822c78f30818fcedbabfb21 languageName: node linkType: hard @@ -9836,15 +9855,15 @@ __metadata: "@emotion/styled": 11.10.5 "@graasp/chatbox": 1.1.0 "@graasp/query-client": 0.4.0 - "@graasp/sdk": 0.9.2 - "@graasp/translations": 1.8.0 + "@graasp/sdk": 0.9.3 + "@graasp/translations": 1.10.0 "@graasp/ui": 2.2.0 "@graasp/websockets": "github:graasp/graasp-websockets.git" "@mui/icons-material": 5.11.0 "@mui/lab": 5.0.0-alpha.117 "@mui/material": 5.11.6 - "@sentry/react": 7.39.0 - "@sentry/tracing": 7.39.0 + "@sentry/react": 7.44.2 + "@sentry/tracing": 7.44.2 "@testing-library/jest-dom": ^5.16.3 "@testing-library/react": ^12.1.4 "@testing-library/user-event": ^13.5.0