Skip to content

Commit

Permalink
feat: build create app cards (#792)
Browse files Browse the repository at this point in the history
* feat: build create app cards

* test: create a test for create a custom app url

* build: add translation for app url input

* fix: remove unrequirred fields from type app card
  • Loading branch information
LinaYahya authored Sep 25, 2023
1 parent 09a317e commit cd8e5f7
Show file tree
Hide file tree
Showing 13 changed files with 209 additions and 90 deletions.
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: '' })
}
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

0 comments on commit cd8e5f7

Please sign in to comment.