Skip to content

Commit

Permalink
feat: add folder redesign amendments (#1207)
Browse files Browse the repository at this point in the history
* feat: add description label

* feat: hide ThumbnailSetting input

* feat: modify tab label text

* feat: add folder thumbnail comp

* feat: add FolderForm

* feat: modify NewItemModal to update folder with thumbnail

* feat: modify thumbnail style

* fix: change how the folder thumbnail is handled

* fix: make proposal

* fix: make changes

* fix: thumbnail test not passing

---------

Co-authored-by: ztlee042 <[email protected]>
  • Loading branch information
spaenleh and ztlee042 authored May 2, 2024
1 parent 4054fc6 commit 0858e44
Show file tree
Hide file tree
Showing 18 changed files with 369 additions and 172 deletions.
13 changes: 7 additions & 6 deletions cypress/e2e/item/settings/thumbnail.cy.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { buildItemSettingsPath } from '../../../../src/config/paths';
import {
CROP_MODAL_CONFIRM_BUTTON_CLASSNAME,
CROP_MODAL_CONFIRM_BUTTON_ID,
ITEM_THUMBNAIL_DELETE_BTN_ID,
THUMBNAIL_SETTING_UPLOAD_BUTTON_CLASSNAME,
THUMBNAIL_SETTING_UPLOAD_INPUT_ID,
} from '../../../../src/config/selectors';
import { SAMPLE_ITEMS } from '../../../fixtures/items';
import {
Expand All @@ -23,13 +23,14 @@ describe('Item Thumbnail', () => {
cy.visit(buildItemSettingsPath(items[0].id));

// change item thumbnail
// selectFile ???
cy.attachFile(
cy.get(`.${THUMBNAIL_SETTING_UPLOAD_BUTTON_CLASSNAME}`),
// target visually hidden input
cy.get(`#${THUMBNAIL_SETTING_UPLOAD_INPUT_ID}`).selectFile(
THUMBNAIL_MEDIUM_PATH,
// use force because the input is visually hidden
{ force: true },
);
cy.wait(FILE_LOADING_PAUSE);
cy.get(`.${CROP_MODAL_CONFIRM_BUTTON_CLASSNAME}`).click();
cy.get(`#${CROP_MODAL_CONFIRM_BUTTON_ID}`).click();
cy.wait(`@uploadItemThumbnail`);
});
});
Expand Down
2 changes: 1 addition & 1 deletion cypress/fixtures/thumbnails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { DEFAULT_FOLDER_ITEM } from './items';
import { MEMBERS } from './members';
import { ITEM_THUMBNAIL_LINK } from './thumbnails/links';

export const THUMBNAIL_MEDIUM_PATH = 'thumbnails/medium.jpeg';
export const THUMBNAIL_MEDIUM_PATH = 'cypress/fixtures/thumbnails/medium.jpeg';

const sampleItems: DiscriminatedItem[] = [
{
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"@emotion/styled": "11.11.5",
"@graasp/chatbox": "3.1.0",
"@graasp/map": "1.11.1",
"@graasp/query-client": "3.5.0",
"@graasp/query-client": "3.6.0",
"@graasp/sdk": "4.7.6",
"@graasp/translations": "1.27.0",
"@graasp/ui": "4.17.0",
Expand Down
4 changes: 2 additions & 2 deletions src/components/common/CropModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { Button } from '@graasp/ui';

import { THUMBNAIL_ASPECT } from '../../config/constants';
import { useBuilderTranslation } from '../../config/i18n';
import { CROP_MODAL_CONFIRM_BUTTON_CLASSNAME } from '../../config/selectors';
import { CROP_MODAL_CONFIRM_BUTTON_ID } from '../../config/selectors';
import { BUILDER } from '../../langs/constants';
import CancelButton from './CancelButton';

Expand Down Expand Up @@ -171,7 +171,7 @@ const CropModal = ({ onConfirm, onClose, src }: CropProps): JSX.Element => {
<CancelButton onClick={onClose} />
<Button
onClick={handleOnConfirm}
className={CROP_MODAL_CONFIRM_BUTTON_CLASSNAME}
id={CROP_MODAL_CONFIRM_BUTTON_ID}
disabled={isError}
>
{t(BUILDER.CONFIRM_BUTTON)}
Expand Down
25 changes: 18 additions & 7 deletions src/components/item/form/DescriptionForm.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Stack } from '@mui/material';
import { Box, FormLabel, Stack, Typography } from '@mui/material';

import {
DescriptionPlacementType,
Expand All @@ -7,6 +7,9 @@ import {
} from '@graasp/sdk';
import TextEditor from '@graasp/ui/text-editor';

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

import DescriptionPlacementForm from './DescriptionPlacementForm';

type DescriptionFormProps = {
Expand All @@ -22,6 +25,7 @@ const DescriptionForm = ({
item,
setChanges,
}: DescriptionFormProps): JSX.Element => {
const { t: translateBuilder } = useBuilderTranslation();
const onChange = (content: string): void => {
setChanges({
description: content,
Expand All @@ -38,12 +42,19 @@ const DescriptionForm = ({

return (
<Stack spacing={2}>
<TextEditor
id={id}
value={(updatedProperties?.description || item?.description) ?? ''}
onChange={onChange}
showActions={false}
/>
<Box>
<FormLabel>
<Typography variant="caption">
{translateBuilder(BUILDER.DESCRIPTION_LABEL)}
</Typography>
</FormLabel>
<TextEditor
id={id}
value={(updatedProperties?.description || item?.description) ?? ''}
onChange={onChange}
showActions={false}
/>
</Box>

{updatedProperties.type !== ItemType.FOLDER && (
<DescriptionPlacementForm
Expand Down
16 changes: 3 additions & 13 deletions src/components/item/form/DisplayNameForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,7 @@ import { ChangeEvent } from 'react';

import ClearIcon from '@mui/icons-material/Clear';
import InfoIcon from '@mui/icons-material/Info';
import {
IconButton,
Stack,
TextField,
Tooltip,
useMediaQuery,
useTheme,
} from '@mui/material';
import { IconButton, Stack, TextField, Tooltip } from '@mui/material';

import { useBuilderTranslation } from '../../../config/i18n';
import { ITEM_FORM_DISPLAY_NAME_INPUT_ID } from '../../../config/selectors';
Expand All @@ -23,9 +16,6 @@ const DisplayNameForm = ({
setChanges,
}: DisplayNameFormProps): JSX.Element => {
const { t: translateBuilder } = useBuilderTranslation();
const theme = useTheme();
// when the screen is large, use only half of the width for the input.
const largeScreen = useMediaQuery(theme.breakpoints.up('sm'));

const handleDisplayNameInput = (event: ChangeEvent<{ value: string }>) => {
setChanges({ displayName: event.target.value });
Expand Down Expand Up @@ -71,8 +61,8 @@ const DisplayNameForm = ({
),
}}
// only take full width when on small screen size
fullWidth={!largeScreen}
sx={{ my: 1, width: largeScreen ? '50%' : undefined }}
fullWidth
sx={{ my: 1 }}
/>
);
};
Expand Down
47 changes: 47 additions & 0 deletions src/components/item/form/FolderForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Stack } from '@mui/material';

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

import { FOLDER_FORM_DESCRIPTION_ID } from '../../../config/selectors';
import DescriptionForm from './DescriptionForm';
import FolderThumbnail from './FolderThumbnail';
import NameForm from './NameForm';

export type FolderFormProps = {
item?: DiscriminatedItem;
setChanges: (
payload: Partial<DiscriminatedItem> & { thumbnail?: Blob },
) => void;
updatedProperties: Partial<DiscriminatedItem> & { thumbnail?: Blob };
};

const FolderForm = ({
item,
updatedProperties,
setChanges,
}: FolderFormProps): JSX.Element => (
<Stack direction="column" gap={2}>
<Stack
direction="row"
justifyContent="flex-start"
alignItems="flex-end"
gap={3}
>
<FolderThumbnail setChanges={setChanges} />
<NameForm
required
setChanges={setChanges}
item={item}
updatedProperties={updatedProperties}
/>
</Stack>
<DescriptionForm
id={FOLDER_FORM_DESCRIPTION_ID}
item={item}
updatedProperties={updatedProperties}
setChanges={setChanges}
/>
</Stack>
);

export default FolderForm;
140 changes: 140 additions & 0 deletions src/components/item/form/FolderThumbnail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { FormEventHandler, useRef, useState } from 'react';

import { Dialog, Stack, styled, useTheme } from '@mui/material';

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

import { ImageUp as ImageUpIcon } from 'lucide-react';

import CropModal, {
MODAL_TITLE_ARIA_LABEL_ID,
} from '@/components/common/CropModal';

const THUMBNAIL_DIMENSION = 60;

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,
});

export type FolderThumbnailProps = {
setChanges: (
payload: Partial<DiscriminatedItem> & { thumbnail?: Blob },
) => void;
};

const FolderThumbnail = ({ setChanges }: FolderThumbnailProps): JSX.Element => {
const inputRef = useRef<HTMLInputElement>(null);
const [showCropModal, setShowCropModal] = useState(false);
const [newAvatar, setNewAvatar] = useState<string>();
const [fileSource, setFileSource] = useState<string>();
const theme = useTheme();

const onSelectFile: FormEventHandler<HTMLInputElement> = (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');
}
// submit cropped image
try {
setChanges({ thumbnail: croppedImage });
// replace img src with croppedImage
const url = URL.createObjectURL(croppedImage);
setNewAvatar(url);
} catch (error) {
console.error(error);
}

return true;
};

const onEdit = () => {
inputRef.current?.click();
};

return (
<Stack justifyContent="flex-start" direction="column">
<Stack
onClick={onEdit}
onKeyDown={(event) => {
if (['Enter', ' '].includes(event.key)) {
onEdit();
}
}}
aria-label="change folder avatar"
role="button"
tabIndex={0}
height={THUMBNAIL_DIMENSION}
width={THUMBNAIL_DIMENSION}
borderRadius={2}
bgcolor="#E4DFFF"
alignItems="center"
justifyContent="center"
overflow="hidden"
position="relative"
sx={{ cursor: 'pointer' }}
>
{newAvatar ? (
<img
alt="folder thumbnail"
src={newAvatar}
height={THUMBNAIL_DIMENSION}
width={THUMBNAIL_DIMENSION}
/>
) : (
<ImageUpIcon color={theme.palette.primary.main} />
)}
</Stack>
<VisuallyHiddenInput
type="file"
accept="image/*"
onChange={onSelectFile}
ref={inputRef}
/>
{fileSource && (
<Dialog
open={showCropModal}
onClose={onClose}
aria-labelledby={MODAL_TITLE_ARIA_LABEL_ID}
>
<CropModal
onClose={onClose}
src={fileSource}
onConfirm={onConfirmCrop}
/>
</Dialog>
)}
</Stack>
);
};

export default FolderThumbnail;
Loading

0 comments on commit 0858e44

Please sign in to comment.