Skip to content

Commit

Permalink
fix: various app list fixes and refinements (#830)
Browse files Browse the repository at this point in the history
fix(ui): scroll in app-list and put app name field bellow list
fix(ui): add a loading state to app list cards
  • Loading branch information
spaenleh authored Oct 16, 2023
1 parent 3cff148 commit b496d59
Show file tree
Hide file tree
Showing 7 changed files with 151 additions and 101 deletions.
121 changes: 79 additions & 42 deletions src/components/item/form/AppForm.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { useState } from 'react';

import { Box, Stack, TextField } from '@mui/material';
import Skeleton from '@mui/material/Skeleton';
import { ArrowBack } from '@mui/icons-material';
import { Alert, Stack, TextField } from '@mui/material';
import Typography from '@mui/material/Typography';
import Grid2 from '@mui/material/Unstable_Grid2/Grid2';

import { DiscriminatedItem } from '@graasp/sdk';
import { DiscriminatedItem, ItemType } from '@graasp/sdk';
import { Button } from '@graasp/ui';

import AppCard from '@/components/main/AppCard';
Expand All @@ -17,6 +17,57 @@ import { BUILDER } from '../../../langs/constants';
import { buildAppExtra } from '../../../utils/itemExtra';
import NameForm from './NameForm';

type AppGridProps = {
currentUrl: string;
handleSelection: (value: null | { name: string; url: string }) => void;
};

const AppGrid = ({
currentUrl,
handleSelection,
}: AppGridProps): JSX.Element | JSX.Element[] => {
const { useApps } = hooks;
const { data, isLoading } = useApps();

const { t: translateBuilder } = useBuilderTranslation();

if (data) {
return (
<>
{data.map((ele) => (
<AppCard
key={ele.name}
name={ele.name}
description={ele.description}
image={ele.extra.image}
selected={ele.url === currentUrl}
onClick={() => {
if (ele.url === currentUrl) {
// reset fields
handleSelection(null);
} else {
handleSelection({ url: ele.url, name: ele.name });
}
}}
/>
))}
</>
);
}

if (isLoading) {
return Array(7)
.fill(0)
.map(() => <AppCard />);
}

return (
<Alert severity="error">
{translateBuilder(BUILDER.APP_LIST_LOADING_FAILED)}
</Alert>
);
};

type Props = {
onChange: (item: Partial<DiscriminatedItem>) => void;
updatedProperties: Partial<DiscriminatedItem>;
Expand Down Expand Up @@ -49,30 +100,37 @@ const AppForm = ({ onChange, updatedProperties = {} }: Props): JSX.Element => {
}
};

const { useApps } = hooks;
const { data, isLoading: isAppsLoading } = useApps();

const currentUrl = (updatedProperties?.extra?.app as { url: string })?.url;
const currentUrl =
(updatedProperties.type === ItemType.APP &&
updatedProperties.extra?.app?.url) ||
'';

const addCustomApp = () => {
setIsCustomApp(true);
// maybe here we would like to not reset the name ?
handleAppSelection(null);
};

if (isAppsLoading) {
return <Skeleton height={60} />;
}
return (
<Box>
<Stack direction="column" height="100%" spacing={2}>
<Typography variant="h6">
{translateBuilder(BUILDER.CREATE_NEW_ITEM_APP_TITLE)}
</Typography>

<NameForm setChanges={onChange} updatedProperties={updatedProperties} />

{isCustomApp ? (
<Stack direction="column" alignItems="start" mt={1} spacing={2}>
<Button
startIcon={<ArrowBack fontSize="small" />}
variant="text"
onClick={() => {
setIsCustomApp(false);
handleAppSelection(null);
}}
>
{translateBuilder(BUILDER.CREATE_NEW_APP_BACK_TO_APP_LIST_BUTTON)}
</Button>
<Typography>
{translateBuilder(BUILDER.CREATE_CUSTOM_APP_HELPER_TEXT)}
</Typography>
<TextField
id={CUSTOM_APP_URL_ID}
fullWidth
Expand All @@ -87,35 +145,13 @@ const AppForm = ({ onChange, updatedProperties = {} }: Props): JSX.Element => {
}
value={currentUrl}
/>
<Button
variant="text"
onClick={() => {
setIsCustomApp(false);
handleAppSelection(null);
}}
>
{translateBuilder(BUILDER.BACK_TO_APP_LIST)}
</Button>
</Stack>
) : (
<Grid2 container spacing={2} alignItems="stretch">
{data?.map((ele) => (
<AppCard
key={ele.name}
name={ele.name}
description={ele.description}
image={ele.extra.image}
selected={ele.url === currentUrl}
onClick={() => {
if (ele.url === currentUrl) {
// reset fields
handleAppSelection(null);
} else {
handleAppSelection({ url: ele.url, name: ele.name });
}
}}
/>
))}
<Grid2 container spacing={2} alignItems="stretch" overflow="scroll">
<AppGrid
currentUrl={currentUrl}
handleSelection={handleAppSelection}
/>
<AppCard
name={translateBuilder(BUILDER.CREATE_CUSTOM_APP)}
description={translateBuilder(
Expand All @@ -125,7 +161,8 @@ const AppForm = ({ onChange, updatedProperties = {} }: Props): JSX.Element => {
/>
</Grid2>
)}
</Box>
<NameForm setChanges={onChange} updatedProperties={updatedProperties} />
</Stack>
);
};

Expand Down
4 changes: 3 additions & 1 deletion src/components/item/form/NameForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ const NameForm = ({
const handleNameInput = (event: ChangeEvent<{ value: string }>) => {
setChanges({ name: event.target.value });
};
// eslint-disable-next-line no-console
console.log('name input', updatedProperties?.name ?? item?.name);

return (
<TextField
Expand All @@ -32,7 +34,7 @@ const NameForm = ({
required={required}
id={ITEM_FORM_NAME_INPUT_ID}
label={translateBuilder(BUILDER.CREATE_NEW_ITEM_NAME_LABEL)}
value={updatedProperties?.name ?? item?.name}
value={updatedProperties?.name ?? item?.name ?? ''}
onChange={handleNameInput}
// always shrink because setting name from defined app does not shrink automatically
InputLabelProps={{ shrink: true }}
Expand Down
108 changes: 60 additions & 48 deletions src/components/main/AppCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,35 @@ import {
CardActionArea,
CardContent,
CardMedia,
Skeleton,
Typography,
styled,
} from '@mui/material';
import Grid2 from '@mui/material/Unstable_Grid2';

import { buildItemFormAppOptionId } from '@/config/selectors';

import AddNewIcon from '../../resources/addNew.png';
const StyledCardActionArea = styled(CardActionArea)({
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
justifyContent: 'flex-start',
height: '100%',
width: '100%',
});

const StyledAppBackground = styled(CardMedia)({
height: 100,
width: '100%',
backgroundColor: '#5050d2',
backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 56 28' width='56' height='28'%3E%3Cpath fill='%239cc4d7' fill-opacity='0.28' d='M56 26v2h-7.75c2.3-1.27 4.94-2 7.75-2zm-26 2a2 2 0 1 0-4 0h-4.09A25.98 25.98 0 0 0 0 16v-2c.67 0 1.34.02 2 .07V14a2 2 0 0 0-2-2v-2a4 4 0 0 1 3.98 3.6 28.09 28.09 0 0 1 2.8-3.86A8 8 0 0 0 0 6V4a9.99 9.99 0 0 1 8.17 4.23c.94-.95 1.96-1.83 3.03-2.63A13.98 13.98 0 0 0 0 0h7.75c2 1.1 3.73 2.63 5.1 4.45 1.12-.72 2.3-1.37 3.53-1.93A20.1 20.1 0 0 0 14.28 0h2.7c.45.56.88 1.14 1.29 1.74 1.3-.48 2.63-.87 4-1.15-.11-.2-.23-.4-.36-.59H26v.07a28.4 28.4 0 0 1 4 0V0h4.09l-.37.59c1.38.28 2.72.67 4.01 1.15.4-.6.84-1.18 1.3-1.74h2.69a20.1 20.1 0 0 0-2.1 2.52c1.23.56 2.41 1.2 3.54 1.93A16.08 16.08 0 0 1 48.25 0H56c-4.58 0-8.65 2.2-11.2 5.6 1.07.8 2.09 1.68 3.03 2.63A9.99 9.99 0 0 1 56 4v2a8 8 0 0 0-6.77 3.74c1.03 1.2 1.97 2.5 2.79 3.86A4 4 0 0 1 56 10v2a2 2 0 0 0-2 2.07 28.4 28.4 0 0 1 2-.07v2c-9.2 0-17.3 4.78-21.91 12H30zM7.75 28H0v-2c2.81 0 5.46.73 7.75 2zM56 20v2c-5.6 0-10.65 2.3-14.28 6h-2.7c4.04-4.89 10.15-8 16.98-8zm-39.03 8h-2.69C10.65 24.3 5.6 22 0 22v-2c6.83 0 12.94 3.11 16.97 8zm15.01-.4a28.09 28.09 0 0 1 2.8-3.86 8 8 0 0 0-13.55 0c1.03 1.2 1.97 2.5 2.79 3.86a4 4 0 0 1 7.96 0zm14.29-11.86c1.3-.48 2.63-.87 4-1.15a25.99 25.99 0 0 0-44.55 0c1.38.28 2.72.67 4.01 1.15a21.98 21.98 0 0 1 36.54 0zm-5.43 2.71c1.13-.72 2.3-1.37 3.54-1.93a19.98 19.98 0 0 0-32.76 0c1.23.56 2.41 1.2 3.54 1.93a15.98 15.98 0 0 1 25.68 0zm-4.67 3.78c.94-.95 1.96-1.83 3.03-2.63a13.98 13.98 0 0 0-22.4 0c1.07.8 2.09 1.68 3.03 2.63a9.99 9.99 0 0 1 16.34 0z'%3E%3C/path%3E%3C/svg%3E")`,
}) as typeof CardMedia;

export type Props = {
description: string;
name: string;
description?: string;
name?: string;
image?: string;
onClick: () => void;
onClick?: () => void;
selected?: boolean;
};

Expand All @@ -26,51 +42,47 @@ const AppCard = ({
onClick,
selected = false,
}: Props): JSX.Element => (
<Card
sx={{
width: '100%',
outline: selected ? '2px solid #5050d2' : '',
}}
onClick={onClick}
id={buildItemFormAppOptionId(name)}
>
<StyledCardActionArea>
<StyledAppBackground
component={image ? 'img' : 'div'}
src={image}
alt={name}
/>
<CardContent sx={{ width: '100%' }}>
<Typography gutterBottom variant="subtitle2" component="div">
{name || <Skeleton />}
</Typography>
<Typography
variant="body2"
color="text.secondary"
sx={{
display: '-webkit-box',
overflow: 'hidden',
WebkitBoxOrient: 'vertical',
WebkitLineClamp: 3,
}}
>
{description || <Skeleton height={45} />}
</Typography>
</CardContent>
</StyledCardActionArea>
</Card>
);
const AppCardWrapper = (props: Props): JSX.Element => (
<Grid2 xs={12} sm={6} md={4} display="flex">
<Card
sx={{
width: '100%',
outline: selected ? '2px solid #5050d2' : '',
}}
onClick={onClick}
id={buildItemFormAppOptionId(name)}
>
<CardActionArea
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
justifyContent: 'flex-start',
height: '100%',
}}
>
<CardMedia
component="img"
// replace with aspectRatio: 1 to display a square image
sx={{ height: 100 }}
image={image || AddNewIcon}
alt={name}
/>
<CardContent>
<Typography gutterBottom variant="subtitle2" component="div">
{name}
</Typography>
<Typography
variant="body2"
color="text.secondary"
sx={{
display: '-webkit-box',
overflow: 'hidden',
WebkitBoxOrient: 'vertical',
WebkitLineClamp: 3,
}}
>
{description}
</Typography>
</CardContent>
</CardActionArea>
</Card>
<AppCard
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
/>
</Grid2>
);

export default AppCard;
export default AppCardWrapper;
8 changes: 1 addition & 7 deletions src/components/main/NewItemModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -249,13 +249,7 @@ const NewItemModal = ({ open, handleClose }: Props): JSX.Element => {
onTypeChange={setSelectedItemType}
initialValue={selectedItemType}
/>
<Box
sx={{
pl: 2,
pr: 2,
width: '100%',
}}
>
<Box px={2} width="100%" overflow="hidden">
{renderContent()}
</Box>
</StyledDialogContent>
Expand Down
2 changes: 1 addition & 1 deletion src/langs/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -257,5 +257,5 @@
"SETTINGS_FILE_SETTINGS_TITLE": "إعدادات الملف",
"APP_URL": "رابط التطبيق",
"CREATE_CUSTOM_APP": "أنشيء تطبيقاً خاصاً",
"BACK_TO_APP_LIST": "الرجوع إلى قائمة التطبيقات"
"CREATE_NEW_APP_BACK_TO_APP_LIST_BUTTON": "الرجوع إلى قائمة التطبيقات"
}
5 changes: 4 additions & 1 deletion src/langs/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,5 +317,8 @@ export const BUILDER = {
APP_URL: 'APP_URL',
CREATE_CUSTOM_APP: 'CREATE_CUSTOM_APP',
CREATE_CUSTOM_APP_DESCRIPTION: 'CREATE_CUSTOM_APP_DESCRIPTION',
BACK_TO_APP_LIST: 'BACK_TO_APP_LIST',
CREATE_CUSTOM_APP_HELPER_TEXT: 'CREATE_CUSTOM_APP_HELPER_TEXT',
CREATE_NEW_APP_BACK_TO_APP_LIST_BUTTON:
'CREATE_NEW_APP_BACK_TO_APP_LIST_BUTTON',
APP_LIST_LOADING_FAILED: 'APP_LIST_LOADING_FAILED',
};
4 changes: 3 additions & 1 deletion src/langs/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -258,5 +258,7 @@
"APP_URL": "App Url",
"CREATE_CUSTOM_APP": "Add Your Custom App",
"CREATE_CUSTOM_APP_DESCRIPTION": "Advanced option to create a custom app",
"BACK_TO_APP_LIST": "Back To App's List"
"CREATE_CUSTOM_APP_HELPER_TEXT": "If you know the URL of an interactive app that can leverage Graasp's API you can input it here.",
"CREATE_NEW_APP_BACK_TO_APP_LIST_BUTTON": "Back To App's List",
"APP_LIST_LOADING_FAILED": "There was an error getting the app list. Please try again later."
}

0 comments on commit b496d59

Please sign in to comment.