From a353659cd8202e15ea559d6388e6338e9cc82540 Mon Sep 17 00:00:00 2001 From: Thibault Reidy <147397675+ReidyT@users.noreply.github.com> Date: Fri, 27 Sep 2024 11:28:47 +0200 Subject: [PATCH] feat: use Thumbnails from PackedItem (#1468) * fix: update ThumbnailUploader to fix flickering warning tooltip * feat: use ThumbnailUploader in ThumbnailSetting and remove ThumbnailWithControls * feat: update ThumbnailCrop's delete button * fix(test): include MUI Tooltip in optimizeDeps of vite.config --- .../e2e/item/authorization/itemLogin/utils.ts | 2 + cypress/e2e/item/move/move.cy.ts | 3 +- cypress/e2e/item/settings/thumbnail.cy.ts | 23 +-- cypress/e2e/item/view/viewFolder.cy.ts | 3 +- cypress/e2e/item/view/viewThumbnails.cy.ts | 8 +- cypress/support/types.ts | 5 +- package.json | 2 +- src/components/item/MapView.tsx | 1 - .../item/publish/PublicationThumbnail.tsx | 4 +- .../item/settings/ThumbnailSetting.tsx | 163 ++++-------------- .../item/settings/ThumbnailWithControls.tsx | 84 --------- src/components/table/ItemCard.tsx | 8 +- src/components/thumbnails/ThumbnailCrop.tsx | 16 +- .../thumbnails/ThumbnailUploader.hook.tsx | 58 +++---- .../thumbnails/ThumbnailUploader.tsx | 70 ++++---- src/config/selectors.ts | 4 - yarn.lock | 56 ++---- 17 files changed, 141 insertions(+), 369 deletions(-) delete mode 100644 src/components/item/settings/ThumbnailWithControls.tsx diff --git a/cypress/e2e/item/authorization/itemLogin/utils.ts b/cypress/e2e/item/authorization/itemLogin/utils.ts index e6c55747b..0fac89c3e 100644 --- a/cypress/e2e/item/authorization/itemLogin/utils.ts +++ b/cypress/e2e/item/authorization/itemLogin/utils.ts @@ -1,6 +1,7 @@ import { ItemLoginSchema, ItemLoginSchemaFactory, + ItemLoginSchemaStatus, ItemLoginSchemaType, PackedItem, } from '@graasp/sdk'; @@ -13,5 +14,6 @@ export const addItemLoginSchema = ( itemLoginSchema: ItemLoginSchemaFactory({ item, type: itemLoginSchemaType, + status: ItemLoginSchemaStatus.Active, }), }); diff --git a/cypress/e2e/item/move/move.cy.ts b/cypress/e2e/item/move/move.cy.ts index 89ec78302..76a6e69ba 100644 --- a/cypress/e2e/item/move/move.cy.ts +++ b/cypress/e2e/item/move/move.cy.ts @@ -208,8 +208,7 @@ describe('Move Items', () => { moveItems({ toItemPath: '' }); cy.wait('@moveItems').then(({ request: { url, body } }) => { - // eslint-disable-next-line @typescript-eslint/no-unused-expressions - expect(body.parentId).to.be.undefined; + expect(body.parentId).equal(undefined); folders.forEach((item) => { expect(url).to.contain(item.id); }); diff --git a/cypress/e2e/item/settings/thumbnail.cy.ts b/cypress/e2e/item/settings/thumbnail.cy.ts index 420a1d616..03f49c022 100644 --- a/cypress/e2e/item/settings/thumbnail.cy.ts +++ b/cypress/e2e/item/settings/thumbnail.cy.ts @@ -1,10 +1,11 @@ -import { PackedFolderItemFactory } from '@graasp/sdk'; +import { PackedFolderItemFactory, PackedItem } from '@graasp/sdk'; import { buildItemSettingsPath } from '../../../../src/config/paths'; import { CROP_MODAL_CONFIRM_BUTTON_ID, - ITEM_THUMBNAIL_DELETE_BTN_ID, - THUMBNAIL_SETTING_UPLOAD_INPUT_ID, + IMAGE_THUMBNAIL_UPLOADER, + REMOVE_THUMBNAIL_BUTTON, + buildDataCyWrapper, } from '../../../../src/config/selectors'; import { ITEM_THUMBNAIL_LINK, @@ -14,9 +15,9 @@ import { FILE_LOADING_PAUSE } from '../../../support/constants'; describe('Item Thumbnail', () => { const item = PackedFolderItemFactory(); - const itemWithThumbnails = { + const itemWithThumbnails: PackedItem = { ...PackedFolderItemFactory(), - thumnails: ITEM_THUMBNAIL_LINK, + thumbnails: { small: ITEM_THUMBNAIL_LINK, medium: ITEM_THUMBNAIL_LINK }, settings: { hasThumbnail: true }, }; beforeEach(() => { @@ -30,7 +31,7 @@ describe('Item Thumbnail', () => { // change item thumbnail // target visually hidden input - cy.get(`#${THUMBNAIL_SETTING_UPLOAD_INPUT_ID}`).selectFile( + cy.get(buildDataCyWrapper(IMAGE_THUMBNAIL_UPLOADER)).selectFile( THUMBNAIL_MEDIUM_PATH, // use force because the input is visually hidden { force: true }, @@ -45,10 +46,10 @@ describe('Item Thumbnail', () => { it('Delete thumbnail button should exist for item with thumbnail', () => { cy.visit(buildItemSettingsPath(itemWithThumbnails.id)); - cy.get(`#${ITEM_THUMBNAIL_DELETE_BTN_ID}`) + cy.get(buildDataCyWrapper(REMOVE_THUMBNAIL_BUTTON)) .invoke('show') - .should('be.visible'); - cy.get(`#${ITEM_THUMBNAIL_DELETE_BTN_ID}`).click(); + .should('be.visible') + .click(); cy.wait(`@deleteItemThumbnail`).then(({ request: { url } }) => { expect(url).to.contain(itemWithThumbnails.id); }); @@ -58,12 +59,12 @@ describe('Item Thumbnail', () => { cy.visit(buildItemSettingsPath(item.id)); Cypress.on('fail', (error) => { expect(error.message).to.include( - `Expected to find element: \`#${ITEM_THUMBNAIL_DELETE_BTN_ID}\`, but never found it.`, + `Expected to find element: \`${buildDataCyWrapper(REMOVE_THUMBNAIL_BUTTON)}\`, but never found it.`, ); return false; // Prevent Cypress from failing the test }); // this will throw an error as ITEM_THUMBNAIL_DELETE_BTN_ID not exist that going to be catches at cypress.on fail - cy.get(`#${ITEM_THUMBNAIL_DELETE_BTN_ID}`).invoke('show'); + cy.get(`${buildDataCyWrapper(REMOVE_THUMBNAIL_BUTTON)}`).invoke('show'); }); }); }); diff --git a/cypress/e2e/item/view/viewFolder.cy.ts b/cypress/e2e/item/view/viewFolder.cy.ts index 1a45ce200..befb6ed58 100644 --- a/cypress/e2e/item/view/viewFolder.cy.ts +++ b/cypress/e2e/item/view/viewFolder.cy.ts @@ -156,12 +156,11 @@ describe('view Folder as admin', () => { cy.get(`#${ITEM_SEARCH_INPUT_ID}`).type(searchText); cy.wait('@getChildren').then(({ request: { query } }) => { - // eslint-disable-next-line @typescript-eslint/no-unused-expressions expect( (query.keywords as unknown as string[]).every((k) => searchText.includes(k), ), - ).to.be.true; + ).equal(true); }); cy.get(`#${buildItemCard(child1.id)}`).should('be.visible'); diff --git a/cypress/e2e/item/view/viewThumbnails.cy.ts b/cypress/e2e/item/view/viewThumbnails.cy.ts index f247cb564..ddccf2b71 100644 --- a/cypress/e2e/item/view/viewThumbnails.cy.ts +++ b/cypress/e2e/item/view/viewThumbnails.cy.ts @@ -1,4 +1,4 @@ -import { PackedFolderItemFactory } from '@graasp/sdk'; +import { PackedFolderItemFactory, PackedItem } from '@graasp/sdk'; import { HOME_PATH } from '../../../../src/config/paths'; import { @@ -11,14 +11,14 @@ import { ITEM_THUMBNAIL_LINK } from '../../../fixtures/thumbnails/links'; const ITEM_WITHOUT_THUMBNAIL = PackedFolderItemFactory({ name: 'own_item_name1', }); -const ITEM_WITH_THUMBNAIL = { +const ITEM_WITH_THUMBNAIL: PackedItem = { ...PackedFolderItemFactory({ name: 'own_item_name2', settings: { hasThumbnail: true, }, }), - thumbnails: ITEM_THUMBNAIL_LINK, + thumbnails: { small: ITEM_THUMBNAIL_LINK, medium: ITEM_THUMBNAIL_LINK }, }; describe('View Thumbnails', () => { @@ -34,7 +34,7 @@ describe('View Thumbnails', () => { cy.get(`#${buildItemCard(ITEM_WITH_THUMBNAIL.id)} img`) .should('have.attr', 'src') - .and('contain', ITEM_WITH_THUMBNAIL.thumbnails); + .and('contain', ITEM_WITH_THUMBNAIL.thumbnails.medium); }); it(`display member avatar`, () => { diff --git a/cypress/support/types.ts b/cypress/support/types.ts index 2eb25bc14..4a5a0c97a 100644 --- a/cypress/support/types.ts +++ b/cypress/support/types.ts @@ -14,18 +14,17 @@ import { ItemTag, ItemValidationGroup, LocalFileItemType, - PackedItem, PermissionLevel, PublicationStatus, RecycledItemData, S3FileItemType, ShortLink, + ThumbnailsBySize, } from '@graasp/sdk'; export type ItemForTest = DiscriminatedItem & { categories?: ItemCategory[]; - // TODO: INCORRECT! Fix in coming - thumbnails?: PackedItem['thumbnails']; + thumbnails?: ThumbnailsBySize; tags?: ItemTag[]; itemLoginSchema?: ItemLoginSchema; readFilepath?: string; diff --git a/package.json b/package.json index 34f46589e..78e5b31f2 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "@emotion/react": "11.13.3", "@emotion/styled": "11.13.0", "@graasp/chatbox": "3.3.0", - "@graasp/map": "1.18.0", + "@graasp/map": "1.19.0", "@graasp/query-client": "3.25.0", "@graasp/sdk": "4.31.0", "@graasp/stylis-plugin-rtl": "2.2.0", diff --git a/src/components/item/MapView.tsx b/src/components/item/MapView.tsx index c5fdce8dd..e1ceb8377 100644 --- a/src/components/item/MapView.tsx +++ b/src/components/item/MapView.tsx @@ -74,7 +74,6 @@ const MapView = ({ useAddressFromGeolocation={hooks.useAddressFromGeolocation} useSuggestionsForAddress={hooks.useSuggestionsForAddress} useItemsInMap={hooks.useItemsInMap} - useItemThumbnailUrl={hooks.useItemThumbnailUrl} viewItem={viewItem} viewItemInBuilder={viewItemInBuilder} currentMember={currentMember} diff --git a/src/components/item/publish/PublicationThumbnail.tsx b/src/components/item/publish/PublicationThumbnail.tsx index 5632f1ce2..9dacb50b3 100644 --- a/src/components/item/publish/PublicationThumbnail.tsx +++ b/src/components/item/publish/PublicationThumbnail.tsx @@ -3,7 +3,7 @@ import { useState } from 'react'; import WarningIcon from '@mui/icons-material/Warning'; import { Tooltip } from '@mui/material'; -import { DiscriminatedItem } from '@graasp/sdk'; +import { PackedItem } from '@graasp/sdk'; import { title } from 'process'; @@ -19,7 +19,7 @@ const THUMBNAIL_SIZE = 150; const SYNC_STATUS_KEY = 'PublicationThumbnail'; type Props = { - item: DiscriminatedItem; + item: PackedItem; thumbnailSize?: number; fullWidth?: boolean; }; diff --git a/src/components/item/settings/ThumbnailSetting.tsx b/src/components/item/settings/ThumbnailSetting.tsx index ad02e096d..8aeed1410 100644 --- a/src/components/item/settings/ThumbnailSetting.tsx +++ b/src/components/item/settings/ThumbnailSetting.tsx @@ -1,146 +1,53 @@ -import { FormEventHandler, useRef, useState } from 'react'; +import { useState } from 'react'; -import { Dialog, Stack, styled } from '@mui/material'; +import { Stack, Typography } from '@mui/material'; -import { - DiscriminatedItem, - ItemType, - ThumbnailSize, - getLinkThumbnailUrl, -} from '@graasp/sdk'; +import { PackedItem } from '@graasp/sdk'; -import { useUploadWithProgress } from '@/components/hooks/uploadWithProgress'; -import { THUMBNAIL_SETTING_UPLOAD_INPUT_ID } from '@/config/selectors'; +import ThumbnailUploader, { + EventChanges, +} from '@/components/thumbnails/ThumbnailUploader'; import { useBuilderTranslation } from '../../../config/i18n'; -import { hooks, mutations } from '../../../config/queryClient'; import { BUILDER } from '../../../langs/constants'; -import CropModal, { MODAL_TITLE_ARIA_LABEL_ID } from '../../common/CropModal'; -import ThumbnailWithControls from './ThumbnailWithControls'; -const VisuallyHiddenInput = styled('input')({ - clip: 'rect(0 0 0 0)', - clipPath: 'inset(50%)', - height: 1, - overflow: 'hidden', - position: 'absolute', - bottom: 0, - left: 0, - whiteSpace: 'nowrap', - width: 1, -}); +const THUMBNAIL_SIZE = 120; +const SYNC_STATUS_KEY = 'ThumbnailSetting'; -type Props = { item: DiscriminatedItem }; +type Props = { item: PackedItem }; const ThumbnailSetting = ({ item }: Props): JSX.Element | null => { - const inputRef = useRef(null); - const [showCropModal, setShowCropModal] = useState(false); - const [fileSource, setFileSource] = useState(); - const { t: translateBuilder } = useBuilderTranslation(); - const { mutateAsync: uploadItemThumbnail } = - mutations.useUploadItemThumbnail(); - const { update, close: closeNotification } = useUploadWithProgress(); - const { mutate: deleteThumbnail } = mutations.useDeleteItemThumbnail(); - const { id: itemId } = item; - const { data: thumbnailUrl, isLoading } = hooks.useItemThumbnailUrl({ - id: itemId, - size: ThumbnailSize.Medium, - }); - - const onSelectFile: FormEventHandler = (e) => { - const t = e.target as HTMLInputElement; - if (t.files && t.files?.length > 0) { - const reader = new FileReader(); - reader.addEventListener('load', () => - setFileSource(reader.result as string), - ); - reader.readAsDataURL(t.files?.[0]); - setShowCropModal(true); - } - }; - - const onClose = () => { - setShowCropModal(false); - if (inputRef.current) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - inputRef.current.value = null; - } - }; - - const onConfirmCrop = (croppedImage: Blob | null) => { - onClose(); - - if (!croppedImage) { - return console.error('croppedImage is not defined'); + const { t } = useBuilderTranslation(); + const [hasThumbnail, setHasThumbnail] = useState(Boolean(item.thumbnails)); + + const handleChange = (e: EventChanges) => { + switch (e) { + case EventChanges.ON_UPLOADING: + case EventChanges.ON_HAS_THUMBNAIL: + setHasThumbnail(true); + break; + case EventChanges.ON_NO_THUMBNAIL: + setHasThumbnail(false); + break; + default: + // nothing to do } - // submit cropped image - try { - // remove waiting files - uploadItemThumbnail({ - // type: croppedImage.type, - file: croppedImage, - id: item.id, - onUploadProgress: update, - }).then(() => { - closeNotification(); - }); - } catch (error) { - console.error(error); - } - - return true; - }; - - const onDelete = () => { - deleteThumbnail(itemId); }; - const onEdit = () => { - inputRef.current?.click(); - }; - - const alt = translateBuilder(BUILDER.THUMBNAIL_SETTING_MY_THUMBNAIL_ALT); - - let imgUrl = thumbnailUrl; - if (!imgUrl && item.type === ItemType.LINK) { - imgUrl = getLinkThumbnailUrl(item.extra); - } - return ( - <> - - - - - {fileSource && ( - - - + + + {!hasThumbnail && ( + + {t(BUILDER.SETTINGS_THUMBNAIL_SETTINGS_INFORMATIONS)} + )} - + ); }; diff --git a/src/components/item/settings/ThumbnailWithControls.tsx b/src/components/item/settings/ThumbnailWithControls.tsx deleted file mode 100644 index 9036c0f8d..000000000 --- a/src/components/item/settings/ThumbnailWithControls.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { Box, IconButton, Stack, Typography, useTheme } from '@mui/material'; - -import { DiscriminatedItem, getMimetype } from '@graasp/sdk'; -import { ItemIcon, Thumbnail } from '@graasp/ui'; - -import { PenIcon, Trash2 } from 'lucide-react'; - -import { useBuilderTranslation } from '@/config/i18n'; -import { - ITEM_THUMBNAIL_DELETE_BTN_ID, - THUMBNAIL_SETTING_UPLOAD_BUTTON_ID, -} from '@/config/selectors'; -import { BUILDER } from '@/langs/constants'; - -import { - THUMBNAIL_SETTING_MAX_HEIGHT, - THUMBNAIL_SETTING_MAX_WIDTH, -} from '../../../config/constants'; - -type Props = { - item: DiscriminatedItem; - isLoading: boolean; - url?: string; - alt: string; - onDelete: () => void; - onEdit: () => void; - hasThumbnail?: boolean; -}; - -const ThumbnailWithDeleteButton = ({ - item, - isLoading, - url, - alt, - onDelete, - onEdit, - hasThumbnail, -}: Props): JSX.Element => { - const theme = useTheme(); - const { t } = useBuilderTranslation(); - return ( - - - - } - url={url ?? undefined} - alt={alt} - maxWidth={THUMBNAIL_SETTING_MAX_WIDTH} - maxHeight={THUMBNAIL_SETTING_MAX_HEIGHT} - sx={{ borderRadius: 2 }} - /> - - {!hasThumbnail && ( - - {t(BUILDER.SETTINGS_THUMBNAIL_SETTINGS_INFORMATIONS)} - - )} - - - - - {hasThumbnail && ( - - - - )} - - - ); -}; - -export default ThumbnailWithDeleteButton; diff --git a/src/components/table/ItemCard.tsx b/src/components/table/ItemCard.tsx index d6d7a62e6..7487cafb8 100644 --- a/src/components/table/ItemCard.tsx +++ b/src/components/table/ItemCard.tsx @@ -4,7 +4,6 @@ import Grid2 from '@mui/material/Unstable_Grid2/Grid2'; import { ItemType, PackedItem, - ThumbnailSize, formatDate, getLinkThumbnailUrl, } from '@graasp/sdk'; @@ -13,7 +12,6 @@ import { Card, TextDisplay } from '@graasp/ui'; import i18n, { useCommonTranslation } from '@/config/i18n'; import { buildItemPath } from '@/config/paths'; -import { hooks } from '@/config/queryClient'; import { ITEM_CARD_CLASS, buildItemCard } from '@/config/selectors'; type Props = { @@ -44,10 +42,8 @@ const ItemCard = ({ onThumbnailClick, }: Props): JSX.Element => { const { t: translateCommon } = useCommonTranslation(); - const { data: thumbnailUrl } = hooks.useItemThumbnailUrl({ - id: showThumbnail ? item.id : undefined, - size: ThumbnailSize.Medium, - }); + + const thumbnailUrl = showThumbnail ? item.thumbnails?.medium : undefined; const dateColumnFormatter = (value: string) => formatDate(value, { diff --git a/src/components/thumbnails/ThumbnailCrop.tsx b/src/components/thumbnails/ThumbnailCrop.tsx index 257e257a2..0afa067d1 100644 --- a/src/components/thumbnails/ThumbnailCrop.tsx +++ b/src/components/thumbnails/ThumbnailCrop.tsx @@ -1,6 +1,5 @@ import { MouseEvent } from 'react'; -import DeleteIcon from '@mui/icons-material/Delete'; import { Box, Dialog, @@ -10,7 +9,7 @@ import { useTheme, } from '@mui/material'; -import { ImageUp as ImageUpIcon } from 'lucide-react'; +import { Trash2 as DeleteIcon, ImageUp as ImageUpIcon } from 'lucide-react'; import CropModal, { MODAL_TITLE_ARIA_LABEL_ID, @@ -54,6 +53,16 @@ const HoveredBox = styled(Stack)(({ zIndex }: { zIndex: number }) => ({ }, })); +const sxDeleteButton = () => { + const bgColor = (opacity: number) => `rgb(255, 255, 255, ${opacity})`; + return { + backgroundColor: bgColor(0.5), + ':hover': { + backgroundColor: bgColor(0.8), + }, + }; +}; + type Props = { setChanges: (payload: { thumbnail?: Blob }) => void; onDelete?: () => void; @@ -107,7 +116,8 @@ const ThumbnailCrop = ({ data-cy={REMOVE_THUMBNAIL_BUTTON} aria-label={t(BUILDER.THUMBNAIL_UPLOADER_DELETE_ARIA_LABEL)} color="error" - size="large" + size="medium" + sx={sxDeleteButton} onClick={handleDelete} > diff --git a/src/components/thumbnails/ThumbnailUploader.hook.tsx b/src/components/thumbnails/ThumbnailUploader.hook.tsx index a4801bde4..54a9eef4e 100644 --- a/src/components/thumbnails/ThumbnailUploader.hook.tsx +++ b/src/components/thumbnails/ThumbnailUploader.hook.tsx @@ -1,15 +1,14 @@ import { useEffect, useState } from 'react'; -import { DiscriminatedItem, ThumbnailSize } from '@graasp/sdk'; +import { PackedItem, ThumbnailSize, ThumbnailsBySize } from '@graasp/sdk'; import { AxiosProgressEvent } from 'axios'; -import { hooks, mutations } from '@/config/queryClient'; +import { mutations } from '@/config/queryClient'; import { useUploadWithProgress } from '../hooks/uploadWithProgress'; const { useDeleteItemThumbnail } = mutations; -const { useItemThumbnailUrl } = hooks; type ItemThumbnail = { hasThumbnail?: boolean; @@ -17,14 +16,13 @@ type ItemThumbnail = { }; type Props = { - item: DiscriminatedItem; - thumbnailSize?: (typeof ThumbnailSize)[keyof typeof ThumbnailSize]; + item: PackedItem; + thumbnailSize?: keyof ThumbnailsBySize; }; type ThumbnailUploadPayload = { thumbnail?: Blob }; type UseThumbnailUploader = { - isThumbnailLoading: boolean; isThumbnailUploading: boolean; isUploadingError: boolean; itemThumbnail: ItemThumbnail; @@ -42,38 +40,33 @@ export const useThumbnailUploader = ({ mutations.useUploadItemThumbnail(); const { update, close: closeNotification } = useUploadWithProgress(); - const { id: itemId, settings } = item; - const { - data: thumbnailUrl, - isInitialLoading, - fetchStatus, - } = useItemThumbnailUrl({ - id: itemId, - size: thumbnailSize, - }); + const { id: itemId } = item; + const thumbnailUrl = item.thumbnails?.[thumbnailSize]; - const [itemThumbnail, setItemThumbnail] = useState({}); + const [itemThumbnail, setItemThumbnail] = useState({ + url: thumbnailUrl, + hasThumbnail: Boolean(thumbnailUrl), + }); const [isThumbnailUploading, setIsThumbnailUploading] = useState(false); const [isUploadingError, setIsUploadingError] = useState(false); const [uploadingProgress, setUploadingProgress] = useState(0); - // shouldLoad is used to avoid having a loading state between the blob and the AWS thumbnail url. - const [shouldLoad, setShouldLoad] = useState(true); - // Because in QueryClient v.4, isInitialLoading is true when query is disabled, checking the fetchStatus is needed. - // https://github.com/TanStack/query/issues/3584 - const isThumbnailLoading = - shouldLoad && isInitialLoading && fetchStatus !== 'idle'; + const updateHasThumbnail = (hasThumbnail: boolean) => + setItemThumbnail((prev) => ({ + ...prev, + hasThumbnail, + })); useEffect(() => { - setItemThumbnail({ + setItemThumbnail((prev) => ({ url: thumbnailUrl, - hasThumbnail: settings.hasThumbnail, - }); - - if (thumbnailUrl) { - setShouldLoad(false); - } - }, [settings, thumbnailUrl]); + // As we set hasThumbnail = true during the upload, + // we only update it if the previous state is false, + // meaning that no upload occured and the item doesn't have a thumbnail. + // This solve some flickering of the warning tooltip after the upload. + hasThumbnail: prev.hasThumbnail || Boolean(thumbnailUrl), + })); + }, [thumbnailUrl]); const handleDelete = () => { setItemThumbnail({ hasThumbnail: false, url: undefined }); @@ -87,6 +80,9 @@ export const useThumbnailUploader = ({ } try { setIsThumbnailUploading(true); + // As we are uploading the thumbnail, we can assume that the item has a thumbnail. + // If an error occur, we have to update the hasThumbnail state. + updateHasThumbnail(true); await uploadItemThumbnail({ id: itemId, file: thumbnail, @@ -99,6 +95,7 @@ export const useThumbnailUploader = ({ } catch (error) { console.error(error); setIsUploadingError(true); + updateHasThumbnail(false); closeNotification(error as Error); } finally { setIsThumbnailUploading(false); @@ -108,7 +105,6 @@ export const useThumbnailUploader = ({ return { isThumbnailUploading, - isThumbnailLoading, isUploadingError, itemThumbnail, uploadingProgress, diff --git a/src/components/thumbnails/ThumbnailUploader.tsx b/src/components/thumbnails/ThumbnailUploader.tsx index eee5a1d4f..b4ef3785c 100644 --- a/src/components/thumbnails/ThumbnailUploader.tsx +++ b/src/components/thumbnails/ThumbnailUploader.tsx @@ -2,10 +2,9 @@ import { useEffect } from 'react'; import { Box, CircularProgress } from '@mui/material'; -import { DiscriminatedItem } from '@graasp/sdk'; +import { PackedItem } from '@graasp/sdk'; import { theme } from '@graasp/ui'; -import ContentLoader from '@/components/common/ContentLoader'; import { useDataSyncContext } from '@/components/context/DataSyncContext'; import ThumbnailCrop from './ThumbnailCrop'; @@ -22,7 +21,7 @@ export enum EventChanges { } type Props = { - item: DiscriminatedItem; + item: PackedItem; thumbnailSize?: number; fullWidth?: boolean; syncStatusKey?: string; @@ -43,7 +42,6 @@ export const ThumbnailUploader = ({ const { uploadingProgress, isThumbnailUploading, - isThumbnailLoading, isUploadingError, itemThumbnail, @@ -71,52 +69,42 @@ export const ThumbnailUploader = ({ case itemThumbnail.hasThumbnail: onChange?.(EventChanges.ON_HAS_THUMBNAIL); break; - case isThumbnailLoading: - onChange?.(EventChanges.ON_LOADING); - break; case !itemThumbnail.hasThumbnail: onChange?.(EventChanges.ON_NO_THUMBNAIL); break; default: // nothing to do } - }, [ - isThumbnailUploading, - itemThumbnail.hasThumbnail, - isThumbnailLoading, - onChange, - ]); + }, [isThumbnailUploading, itemThumbnail.hasThumbnail, onChange]); return ( - - - - - {topCornerElement} - {isThumbnailUploading && ( - - )} - + + + + {topCornerElement} + {isThumbnailUploading && ( + + )} - + ); }; diff --git a/src/config/selectors.ts b/src/config/selectors.ts index 6938a0d77..a7482dce9 100644 --- a/src/config/selectors.ts +++ b/src/config/selectors.ts @@ -183,7 +183,6 @@ export const ITEM_CARD_MEDIA_CLASSNAME = 'itemCardMedia'; export const ITEM_CARD_HEADER_CLASSNAME = 'itemCardHeader'; export const THUMBNAIL_SETTING_UPLOAD_BUTTON_ID = 'thumbnailSettingUploadButton'; -export const THUMBNAIL_SETTING_UPLOAD_INPUT_ID = 'thumbnailSettingUploadInput'; export const CLEAR_CHAT_SETTING_ID = 'clearChatSettingButton'; export const CLEAR_CHAT_DIALOG_ID = 'clearChatDialog'; export const CLEAR_CHAT_CANCEL_BUTTON_ID = 'clearChatCancelButton'; @@ -364,9 +363,6 @@ export const buildDescriptionPlacementId = ( placement: DescriptionPlacementType, ): string => `itemSettingDescriptionPlacement-${placement}`; -export const ITEM_THUMBNAIL_CONTAINER_ID = 'itemThumbnailContainer'; -export const ITEM_THUMBNAIL_DELETE_BTN_ID = 'itemThumbnailDeleteBtn'; - export const DROPZONE_SELECTOR = '[role="dropzone"]'; export const buildMapViewId = (parentId?: string): string => `map-view-${parentId}`; diff --git a/yarn.lock b/yarn.lock index b0a3e1b34..732a8ee62 100644 --- a/yarn.lock +++ b/yarn.lock @@ -41,7 +41,7 @@ __metadata: languageName: node linkType: hard -"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.22.13, @babel/code-frame@npm:^7.24.7": +"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.22.13": version: 7.24.7 resolution: "@babel/code-frame@npm:7.24.7" dependencies: @@ -172,7 +172,7 @@ __metadata: languageName: node linkType: hard -"@babel/generator@npm:^7.23.0, @babel/generator@npm:^7.25.6": +"@babel/generator@npm:^7.23.0": version: 7.25.6 resolution: "@babel/generator@npm:7.25.6" dependencies: @@ -299,17 +299,7 @@ __metadata: languageName: node linkType: hard -"@babel/helper-module-imports@npm:^7.16.7": - version: 7.24.7 - resolution: "@babel/helper-module-imports@npm:7.24.7" - dependencies: - "@babel/traverse": "npm:^7.24.7" - "@babel/types": "npm:^7.24.7" - checksum: 10/df8bfb2bb18413aa151ecd63b7d5deb0eec102f924f9de6bc08022ced7ed8ca7fed914562d2f6fa5b59b74a5d6e255dc35612b2bc3b8abf361e13f61b3704770 - languageName: node - linkType: hard - -"@babel/helper-module-imports@npm:^7.22.15": +"@babel/helper-module-imports@npm:^7.16.7, @babel/helper-module-imports@npm:^7.22.15": version: 7.24.3 resolution: "@babel/helper-module-imports@npm:7.24.3" dependencies: @@ -553,7 +543,7 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.20.5, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.23.0, @babel/parser@npm:^7.23.9, @babel/parser@npm:^7.25.0, @babel/parser@npm:^7.25.6": +"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.20.5, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.23.0, @babel/parser@npm:^7.23.9": version: 7.25.6 resolution: "@babel/parser@npm:7.25.6" dependencies: @@ -671,17 +661,6 @@ __metadata: languageName: node linkType: hard -"@babel/template@npm:^7.25.0": - version: 7.25.0 - resolution: "@babel/template@npm:7.25.0" - dependencies: - "@babel/code-frame": "npm:^7.24.7" - "@babel/parser": "npm:^7.25.0" - "@babel/types": "npm:^7.25.0" - checksum: 10/07ebecf6db8b28244b7397628e09c99e7a317b959b926d90455c7253c88df3677a5a32d1501d9749fe292a263ff51a4b6b5385bcabd5dadd3a48036f4d4949e0 - languageName: node - linkType: hard - "@babel/traverse@npm:7.23.2": version: 7.23.2 resolution: "@babel/traverse@npm:7.23.2" @@ -736,21 +715,6 @@ __metadata: languageName: node linkType: hard -"@babel/traverse@npm:^7.24.7": - version: 7.25.6 - resolution: "@babel/traverse@npm:7.25.6" - dependencies: - "@babel/code-frame": "npm:^7.24.7" - "@babel/generator": "npm:^7.25.6" - "@babel/parser": "npm:^7.25.6" - "@babel/template": "npm:^7.25.0" - "@babel/types": "npm:^7.25.6" - debug: "npm:^4.3.1" - globals: "npm:^11.1.0" - checksum: 10/de75a918299bc27a44ec973e3f2fa8c7902bbd67bd5d39a0be656f3c1127f33ebc79c12696fbc8170a0b0e1072a966d4a2126578d7ea2e241b0aeb5d16edc738 - languageName: node - linkType: hard - "@babel/types@npm:7.17.0": version: 7.17.0 resolution: "@babel/types@npm:7.17.0" @@ -794,7 +758,7 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.24.7, @babel/types@npm:^7.25.0, @babel/types@npm:^7.25.6": +"@babel/types@npm:^7.25.6": version: 7.25.6 resolution: "@babel/types@npm:7.25.6" dependencies: @@ -1748,9 +1712,9 @@ __metadata: languageName: node linkType: hard -"@graasp/map@npm:1.18.0": - version: 1.18.0 - resolution: "@graasp/map@npm:1.18.0" +"@graasp/map@npm:1.19.0": + version: 1.19.0 + resolution: "@graasp/map@npm:1.19.0" dependencies: i18n-iso-countries: "npm:7.11.2" leaflet: "npm:^1.9.4" @@ -1774,7 +1738,7 @@ __metadata: react: "*" react-dom: "*" react-i18next: ^14.0.0 - checksum: 10/2da1fd3e4f82d0bf9537d2ef48660dff3babff364776f4a0e4a0ce0eabd809a96ed02eae13ca4d22d039f234e3979216059e985f6c7e96733d2f48a315383364 + checksum: 10/3e181cbcdb07ee256bc1fbbe1f57b5d32192446a767f76b8a0a85e681f42d9e94a8101c27d1cc5e8523b4a97c885a9208d1f8d199c0fe0cb5ff55700c383f09f languageName: node linkType: hard @@ -6595,7 +6559,7 @@ __metadata: "@emotion/react": "npm:11.13.3" "@emotion/styled": "npm:11.13.0" "@graasp/chatbox": "npm:3.3.0" - "@graasp/map": "npm:1.18.0" + "@graasp/map": "npm:1.19.0" "@graasp/query-client": "npm:3.25.0" "@graasp/sdk": "npm:4.31.0" "@graasp/stylis-plugin-rtl": "npm:2.2.0"