Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: build create app cards #792

Merged
merged 4 commits into from
Sep 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions cypress/e2e/item/create/createApp.cy.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { HOME_PATH, buildItemPath } from '../../../../src/config/paths';
import ITEM_LAYOUT_MODES from '../../../../src/enums/itemLayoutModes';
import { GRAASP_APP_ITEM } from '../../../fixtures/apps';
import {
GRAASP_APP_ITEM,
GRAASP_CUSTOM_APP_ITEM,
} from '../../../fixtures/apps';
import { SAMPLE_ITEMS } from '../../../fixtures/items';
import { createApp } from '../../../support/createUtils';

Expand Down Expand Up @@ -56,7 +59,7 @@ describe('Create App', () => {
});
});

it('Create app by typing', () => {
it('Create a custom app', () => {
cy.setUpApi(SAMPLE_ITEMS);
const { id } = SAMPLE_ITEMS.items[0];

Expand All @@ -66,7 +69,7 @@ describe('Create App', () => {
cy.switchMode(ITEM_LAYOUT_MODES.LIST);

// create
createApp(GRAASP_APP_ITEM);
createApp(GRAASP_CUSTOM_APP_ITEM, { custom: true });

cy.wait('@postItem').then(() => {
// expect update
Expand Down
14 changes: 13 additions & 1 deletion cypress/fixtures/apps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,19 @@ export const GRAASP_APP_ITEM: AppItemType = {
...DEFAULT_FOLDER_ITEM,
id: 'ecafbd2a-5688-12eb-ae91-0272ac130002',
path: 'ecafbd2a_5688_12eb_ae91_0272ac130002',
name: 'my app',
name: 'test app',
description: 'my app description',
type: ItemType.APP,
extra: {
[ItemType.APP]: { url: APPS_LIST[0].url },
},
creator: CURRENT_USER,
};
export const GRAASP_CUSTOM_APP_ITEM: AppItemType = {
...DEFAULT_FOLDER_ITEM,
id: 'ecafbd2a-5688-12eb-ae91-0272ac130002',
path: 'ecafbd2a_5688_12eb_ae91_0272ac130002',
name: 'Add Your Custom App',
description: 'my app description',
type: ItemType.APP,
extra: {
Expand Down
1 change: 1 addition & 0 deletions cypress/fixtures/apps/apps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { App, Publisher } from '@graasp/sdk';

export const APP_NAME = 'test app';
export const NEW_APP_NAME = 'my new test app';
export const CUSTOM_APP_URL = 'http://testapp.com';

export const publisher: Publisher = {
id: 'publisher-id',
Expand Down
19 changes: 15 additions & 4 deletions cypress/support/commands/item.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ItemType, getAppExtra, getDocumentExtra } from '@graasp/sdk';

import {
CUSTOM_APP_URL_ID,
FOLDER_FORM_DESCRIPTION_ID,
ITEM_FORM_APP_URL_ID,
ITEM_FORM_CONFIRM_BUTTON_ID,
Expand All @@ -17,7 +18,11 @@ import {
buildTreeItemId,
} from '../../../src/config/selectors';
import { getParentsIdsFromPath } from '../../../src/utils/item';
import { APP_NAME, NEW_APP_NAME } from '../../fixtures/apps/apps';
import {
APP_NAME,
CUSTOM_APP_URL,
NEW_APP_NAME,
} from '../../fixtures/apps/apps';
import { TREE_VIEW_PAUSE } from '../constants';

Cypress.Commands.add(
Expand Down Expand Up @@ -137,14 +142,20 @@ Cypress.Commands.add(

Cypress.Commands.add(
'fillAppModal',
({ name = '', extra }, { confirm = true, type = false } = {}) => {
(
{ name = '', extra },
{ confirm = true, type = false, custom = false } = {},
) => {
cy.fillBaseItemModal({ name }, { confirm: false });

cy.get(`#${ITEM_FORM_APP_URL_ID}`).click();
if (type) {
cy.get(`#${ITEM_FORM_APP_URL_ID}`).type(getAppExtra(extra)?.url);
} else if (custom) {
cy.get(`#${buildItemFormAppOptionId(name)}`).click();
// check name get added automatically
cy.get(`#${CUSTOM_APP_URL_ID}`).type(CUSTOM_APP_URL);
} else {
cy.get(`#${buildItemFormAppOptionId(APP_NAME)}`).click();
cy.get(`#${buildItemFormAppOptionId(name)}`).click();
// check name get added automatically
cy.get(`#${ITEM_FORM_NAME_INPUT_ID}`).should('have.value', APP_NAME);
// edit the app name
Expand Down
2 changes: 1 addition & 1 deletion cypress/support/createUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { FileItemForTest } from './types';

export const createApp = (
payload: AppItemType,
options?: { confirm?: boolean },
options?: { confirm?: boolean; custom?: boolean },
): void => {
cy.get(`#${CREATE_ITEM_BUTTON_ID}`).click();
cy.get(`#${CREATE_ITEM_APP_ID}`).click();
Expand Down
2 changes: 1 addition & 1 deletion cypress/support/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ declare global {
): void;
fillAppModal(
payload: { name: string; extra?: AppItemExtra },
options?: { type?: boolean; confirm?: boolean },
options?: { type?: boolean; confirm?: boolean; custom?: boolean },
): void;
fillFolderModal(
arg1: { name?: string; description?: string },
Expand Down
147 changes: 69 additions & 78 deletions src/components/item/form/AppForm.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
import { HTMLAttributes, useState } from 'react';
import React, { useState } from 'react';

import { TextField } from '@mui/material';
import Autocomplete from '@mui/material/Autocomplete';
import CloseIcon from '@mui/icons-material/Close';
import { Box, IconButton, InputAdornment, TextField } from '@mui/material';
import Skeleton from '@mui/material/Skeleton';
import Typography from '@mui/material/Typography';

import { AppItemType, DiscriminatedItem, Item, getAppExtra } from '@graasp/sdk';
import { AppItemType, DiscriminatedItem } from '@graasp/sdk';
import { AppRecord } from '@graasp/sdk/frontend';

import AppCard from '@/components/main/AppCard';
import { CUSTOM_APP_URL_ID } from '@/config/selectors';

import { useBuilderTranslation } from '../../../config/i18n';
import { hooks } from '../../../config/queryClient';
import {
ITEM_FORM_APP_URL_ID,
buildItemFormAppOptionId,
} from '../../../config/selectors';
import { BUILDER } from '../../../langs/constants';
import { buildAppExtra } from '../../../utils/itemExtra';
import BaseItemForm from './NameForm';
Expand All @@ -31,8 +30,11 @@ const AppForm = ({
}: Props): JSX.Element => {
const { t: translateBuilder } = useBuilderTranslation();
const [newName, setNewName] = useState<string>(item?.name ?? '');
const [isCustomApp, setIsCustomApp] = useState<boolean>(false);

const handleAppSelection = (_event: any, newValue: AppRecord | null) => {
const handleAppSelection = (
newValue: AppRecord | null | { url: string; name: string },
) => {
if (!newValue) {
return console.error('new value is undefined');
}
Expand All @@ -51,23 +53,19 @@ const AppForm = ({
return onChange(props);
};

const handleAppInput = (_event: any, url: string) => {
// TODO: improve types
const props = {
...item,
extra: buildAppExtra({ url }),
} as unknown as Item;
onChange(props);
};

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

const url = getAppExtra(item?.extra)?.url;
const url = (updatedProperties?.extra?.app as { url: string })?.url;

// todo: fix type -> we will change the interface
const value = data?.find((app) => app.url === url) || (url as any);
const addCustomApp = () => {
setIsCustomApp(true);
handleAppSelection({ url: '', name: '' });
};

if (isAppsLoading) {
return <Skeleton height={60} />;
}
return (
<div>
<Typography variant="h6">
Expand All @@ -83,67 +81,60 @@ const AppForm = ({
} as Partial<DiscriminatedItem>
}
/>
<br />

{isAppsLoading ? (
<Skeleton height={60} />
) : (
<Autocomplete
id={ITEM_FORM_APP_URL_ID}
options={data?.toArray() ?? []}
getOptionLabel={(option) => {
if (typeof option === 'string') {
return option;
{isCustomApp ? (
<Box sx={{ mt: 3 }}>
<TextField
id={CUSTOM_APP_URL_ID}
fullWidth
variant="standard"
autoFocus
label={translateBuilder(BUILDER.APP_URL)}
onChange={(e) =>
handleAppSelection({ url: e.target.value, name: '' })
LinaYahya marked this conversation as resolved.
Show resolved Hide resolved
}
return option.url;
}}
filterOptions={(options, state) => {
const filteredOptionsByName = options.filter((opt: AppRecord) =>
opt.name.toLowerCase().includes(state.inputValue.toLowerCase()),
);
return filteredOptionsByName;
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
onClick={() => {
setIsCustomApp(false);
handleAppSelection({ url: '', name: '' });
}}
>
<CloseIcon />
</IconButton>
</InputAdornment>
),
}}
/>
</Box>
) : (
<Box
sx={{
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gap: 2,
mt: 3,
}}
value={value}
clearOnBlur={false}
onChange={handleAppSelection}
onInputChange={handleAppInput}
renderOption={(
props: HTMLAttributes<HTMLLIElement>,
option: AppRecord,
) => (
<li
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
style={{
display: 'flex',
padding: 8,
alignItems: 'center',
}}
id={buildItemFormAppOptionId(option.name)}
>
<img
style={{
verticalAlign: 'middle',
margin: 8,
height: '30px',
}}
src={option.extra?.image as string}
alt={option.name}
/>
<Typography variant="body1" pr={1}>
{option.name}
</Typography>
<Typography variant="caption">{option.description}</Typography>
</li>
)}
renderInput={(params) => (
<TextField
variant="standard"
// eslint-disable-next-line react/jsx-props-no-spreading
{...params}
label={translateBuilder(BUILDER.CREATE_NEW_ITEM_APP_URL_LABEL)}
>
{data?.map((ele) => (
<AppCard
key={ele.name}
url={ele?.url}
name={ele.name}
description={ele.description}
extra={ele?.extra}
selected={ele?.url === url}
handleSelect={handleAppSelection}
/>
)}
/>
))}
<AppCard
name={translateBuilder(BUILDER.CREATE_CUSTOM_APP)}
handleSelect={addCustomApp}
/>
</Box>
)}
</div>
);
Expand Down
Loading