// SPDX-License-Identifier: AGPL-3.0
import React from 'react';
-import { StyleRulesCallback, Card, CardHeader, WithStyles, withStyles, Typography, CardContent, Tooltip, Collapse } from '@material-ui/core';
+import { StyleRulesCallback, Card, CardHeader, WithStyles, withStyles, Typography, CardContent, Tooltip, Collapse, Grid } from '@material-ui/core';
import { ArvadosTheme } from 'common/custom-theme';
import { RootState } from 'store/store';
import { connect } from 'react-redux';
import { UserResource } from 'models/user';
import { UserResourceAccountStatus } from 'views-components/data-explorer/renderers';
import { FavoriteStar, PublicFavoriteStar } from 'views-components/favorite-star/favorite-star';
-import { MoreVerticalIcon, FreezeIcon } from 'components/icon/icon';
+import { FreezeIcon } from 'components/icon/icon';
import { Resource } from 'models/resource';
-import { IconButton } from '@material-ui/core';
-import { ContextMenuResource } from 'store/context-menu/context-menu-actions';
-import { openContextMenu, resourceUuidToContextMenuKind } from 'store/context-menu/context-menu-actions';
-import { CollectionResource } from 'models/collection';
-import { ContextMenuKind } from 'views-components/context-menu/context-menu';
import { Dispatch } from 'redux';
-import classNames from 'classnames';
import { loadDetailsPanel } from 'store/details-panel/details-panel-action';
import { ExpandChevronRight } from 'components/expand-chevron-right/expand-chevron-right';
+import { MultiselectToolbar } from 'components/multiselect-toolbar/MultiselectToolbar';
+import { setSelectedResourceUuid } from 'store/selected-resource/selected-resource-actions';
+import { deselectAllOthers } from 'store/multiselect/multiselect-actions';
type CssRules =
| 'root'
- | 'selected'
+ | 'cardHeaderContainer'
| 'cardHeader'
+ | 'projectToolbar'
| 'descriptionToggle'
| 'showMore'
| 'noDescription'
| 'namePlate'
| 'faveIcon'
| 'frozenIcon'
- | 'contextMenuSection'
+ | 'accountStatusSection'
+ | 'chipSection'
| 'tag'
| 'description';
marginBottom: '1rem',
flex: '0 0 auto',
padding: 0,
- border: '2px solid transparent',
- },
- selected: {
- border: '2px solid #ccc',
+ minHeight: '3rem',
},
showMore: {
cursor: 'pointer',
- background: 'linear-gradient(to right, black, transparent)',
- backgroundClip: 'text',
- color: 'transparent',
},
noDescription: {
color: theme.palette.grey['600'],
fontStyle: 'italic',
+ padding: '0 0 0.5rem 1rem',
+ marginTop: '-0.5rem',
},
userNameContainer: {
display: 'flex',
+ alignItems: 'center',
+ minHeight: '2.7rem',
+ },
+ cardHeaderContainer: {
+ width: '100%',
+ display: 'flex',
+ flexDirection: 'row',
+ justifyContent: 'space-between',
},
cardHeader: {
- padding: '0.2rem 0.4rem 0.1rem 1rem',
+ minWidth: '30rem',
+ padding: '0.2rem 0.4rem 0.2rem 1rem',
+ },
+ projectToolbar: {
+ //shows only the first 3 buttons
+ width: '12rem !important',
},
descriptionToggle: {
display: 'flex',
cardContent: {
display: 'flex',
flexDirection: 'column',
- marginTop: '-1.75rem',
+ paddingTop: 0,
+ paddingLeft: '0.1rem',
},
nameSection: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
- justifyContent: 'space-between',
},
namePlate: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
margin: 0,
- paddingBottom: '0.5rem',
+ minHeight: '2.7rem',
},
faveIcon: {
fontSize: '0.8rem',
- margin: 'auto 0 0.5rem 0.3rem',
+ margin: 'auto 0 1rem 0.3rem',
color: theme.palette.text.primary,
},
frozenIcon: {
fontSize: '0.5rem',
marginLeft: '0.3rem',
- marginTop: '0.1rem',
height: '1rem',
color: theme.palette.text.primary,
},
- contextMenuSection: {
+ accountStatusSection: {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
- paddingTop: '0.25rem',
+ paddingLeft: '1rem',
+ },
+ chipSection: {
+ marginBottom: '-2rem',
},
tag: {
- marginRight: '1rem',
- marginTop: '1rem',
+ marginRight: '0.75rem',
+ marginBottom: '0.5rem',
},
description: {
- maxWidth: '90%',
+ maxWidth: '95%',
marginTop: 0,
},
});
-const mapStateToProps = (state: RootState) => {
- const currentRoute = state.router.location?.pathname.split('/') || [];
- const currentItemUuid = currentRoute[currentRoute.length - 1];
- const currentResource = getResource(currentItemUuid)(state.resources);
- const frozenByUser = currentResource && getResource((currentResource as ProjectResource).frozenByUuid as string)(state.resources);
+const mapStateToProps = ({ auth, selectedResourceUuid, resources, properties }: RootState) => {
+ const currentResource = getResource(properties.currentRouteUuid)(resources);
+ const frozenByUser = currentResource && getResource((currentResource as ProjectResource).frozenByUuid as string)(resources);
const frozenByFullName = frozenByUser && (frozenByUser as Resource & { fullName: string }).fullName;
- const isSelected = currentItemUuid === state.detailsPanel.resourceUuid && state.detailsPanel.isOpened === true;
+ const isSelected = selectedResourceUuid === properties.currentRouteUuid;
return {
- isAdmin: state.auth.user?.isAdmin,
+ isAdmin: auth.user?.isAdmin,
currentResource,
frozenByFullName,
isSelected,
const mapDispatchToProps = (dispatch: Dispatch) => ({
handleCardClick: (uuid: string) => {
dispatch<any>(loadDetailsPanel(uuid));
+ dispatch<any>(setSelectedResourceUuid(uuid));
+ dispatch<any>(deselectAllOthers(uuid));
},
- handleContextMenu: (event: React.MouseEvent<HTMLElement>, resource: any, isAdmin: boolean) => {
- event.stopPropagation();
- // When viewing the contents of a filter group, all contents should be treated as read only.
- let readOnly = false;
- if (resource.groupClass === 'filter') {
- readOnly = true;
- }
- let menuKind = dispatch<any>(resourceUuidToContextMenuKind(resource.uuid, readOnly));
- if (menuKind === ContextMenuKind.ROOT_PROJECT) {
- menuKind = ContextMenuKind.USER_DETAILS;
- }
- if (menuKind && resource) {
- dispatch<any>(
- openContextMenu(event, {
- name: resource.name,
- uuid: resource.uuid,
- ownerUuid: resource.ownerUuid,
- isTrashed: 'isTrashed' in resource ? resource.isTrashed : false,
- kind: resource.kind,
- menuKind,
- isAdmin,
- isFrozen: !!resource.frozenByUuid,
- description: resource.description,
- storageClassesDesired: (resource as CollectionResource).storageClassesDesired,
- properties: 'properties' in resource ? resource.properties : {},
- })
- );
- }
- },
+
});
type DetailsCardProps = WithStyles<CssRules> & {
frozenByFullName?: string;
isAdmin: boolean;
isSelected: boolean;
- handleContextMenu: (event: React.MouseEvent<HTMLElement>, resource: ContextMenuResource, isAdmin: boolean) => void;
handleCardClick: (resource: any) => void;
};
currentResource: UserResource;
isAdmin: boolean;
isSelected: boolean;
- handleContextMenu: (event: React.MouseEvent<HTMLElement>, resource: ContextMenuResource, isAdmin: boolean) => void;
handleCardClick: (resource: any) => void;
};
frozenByFullName: string | undefined;
isAdmin: boolean;
isSelected: boolean;
- handleContextMenu: (event: React.MouseEvent<HTMLElement>, resource: ContextMenuResource, isAdmin: boolean) => void;
handleCardClick: (resource: any) => void;
};
mapDispatchToProps
)(
withStyles(styles)((props: DetailsCardProps) => {
- const { classes, currentResource, frozenByFullName, handleContextMenu, handleCardClick, isAdmin, isSelected } = props;
+ const { classes, currentResource, frozenByFullName, handleCardClick, isAdmin, isSelected } = props;
if (!currentResource) {
return null;
}
currentResource={currentResource as UserResource}
isAdmin={isAdmin}
isSelected={isSelected}
- handleContextMenu={(ev) => handleContextMenu(ev, currentResource as any, isAdmin)}
handleCardClick={handleCardClick}
/>
);
frozenByFullName={frozenByFullName}
isAdmin={isAdmin}
isSelected={isSelected}
- handleContextMenu={(ev) => handleContextMenu(ev, currentResource as any, isAdmin)}
handleCardClick={handleCardClick}
/>
);
})
);
-const UserCard: React.FC<UserCardProps> = ({ classes, currentResource, handleContextMenu, handleCardClick, isAdmin, isSelected }) => {
+const UserCard: React.FC<UserCardProps> = ({ classes, currentResource, handleCardClick, isSelected }) => {
const { fullName, uuid } = currentResource as UserResource & { fullName: string };
return (
<Card
- className={classNames(classes.root, isSelected ? classes.selected : '')}
+ className={classes.root}
onClick={() => handleCardClick(uuid)}
data-cy='user-details-card'
>
- <CardHeader
- className={classes.cardHeader}
- title={
- <section className={classes.userNameContainer}>
- <Typography
- noWrap
- variant='h6'
- >
- {fullName}
- </Typography>
- </section>
- }
- action={
- <section className={classes.contextMenuSection}>
- {!currentResource.isActive && (
- <Typography>
- <UserResourceAccountStatus uuid={uuid} />
- </Typography>
- )}
- <Tooltip
- title='More options'
- disableFocusListener
- >
- <IconButton
- aria-label='More options'
- data-cy='kebab-icon'
- onClick={(ev) => handleContextMenu(ev, currentResource as any, isAdmin)}
+ <Grid
+ container
+ wrap='nowrap'
+ className={classes.cardHeaderContainer}
+ >
+ <CardHeader
+ className={classes.cardHeader}
+ title={
+ <section className={classes.userNameContainer}>
+ <Typography
+ noWrap
+ variant='h6'
>
- <MoreVerticalIcon />
- </IconButton>
- </Tooltip>
- </section>
- }
- />
+ {fullName}
+ </Typography>
+ <section className={classes.accountStatusSection}>
+ {!currentResource.isActive && (
+ <Typography>
+ <UserResourceAccountStatus uuid={uuid} />
+ </Typography>
+ )}
+ </section>
+ </section>
+ }
+ />
+ {isSelected && <MultiselectToolbar />}
+ </Grid>
</Card>
);
};
-const ProjectCard: React.FC<ProjectCardProps> = ({ classes, currentResource, frozenByFullName, handleContextMenu, handleCardClick, isAdmin, isSelected }) => {
+const ProjectCard: React.FC<ProjectCardProps> = ({ classes, currentResource, frozenByFullName, handleCardClick, isSelected }) => {
const { name, description, uuid } = currentResource as ProjectResource;
const [showDescription, setShowDescription] = React.useState(false);
const [showProperties, setShowProperties] = React.useState(false);
return (
<Card
- className={classNames(classes.root, isSelected ? classes.selected : '')}
+ className={classes.root}
onClick={() => handleCardClick(uuid)}
data-cy='project-details-card'
>
- <CardHeader
- className={classes.cardHeader}
- title={
- <section className={classes.nameSection}>
- <section className={classes.namePlate}>
- <Typography
- noWrap
- variant='h6'
- style={{ marginRight: '1rem' }}
- >
- {name}
- </Typography>
- <FavoriteStar
- className={classes.faveIcon}
- resourceUuid={currentResource.uuid}
- />
- <PublicFavoriteStar
- className={classes.faveIcon}
- resourceUuid={currentResource.uuid}
- />
- {!!frozenByFullName && (
- <Tooltip
- className={classes.frozenIcon}
- disableFocusListener
- title={<span>Project was frozen by {frozenByFullName}</span>}
+ <Grid
+ container
+ wrap='nowrap'
+ className={classes.cardHeaderContainer}
+ >
+ <CardHeader
+ className={classes.cardHeader}
+ title={
+ <section className={classes.nameSection}>
+ <section className={classes.namePlate}>
+ <Typography
+ variant='h6'
+ style={{ marginRight: '1rem' }}
>
- <FreezeIcon style={{ fontSize: 'inherit' }} />
- </Tooltip>
- )}
+ {name}
+ </Typography>
+ <FavoriteStar
+ className={classes.faveIcon}
+ resourceUuid={currentResource.uuid}
+ />
+ <PublicFavoriteStar
+ className={classes.faveIcon}
+ resourceUuid={currentResource.uuid}
+ />
+ {!!frozenByFullName && (
+ <Tooltip
+ className={classes.frozenIcon}
+ disableFocusListener
+ title={<span>Project was frozen by {frozenByFullName}</span>}
+ >
+ <FreezeIcon style={{ fontSize: 'inherit' }} />
+ </Tooltip>
+ )}
+ </section>
</section>
- </section>
- }
- action={
- <section className={classes.contextMenuSection}>
- <Tooltip
- title='More options'
- disableFocusListener
- >
- <IconButton
- aria-label='More options'
- onClick={(ev) => handleContextMenu(ev, currentResource as any, isAdmin)}
- >
- <MoreVerticalIcon data-cy='kebab-icon' />
- </IconButton>
- </Tooltip>
- </section>
- }
- />
+ }
+ />
+ {isSelected && <MultiselectToolbar injectedStyles={classes.projectToolbar} />}
+ </Grid>
<section onClick={(ev) => ev.stopPropagation()}>
{description ? (
<section
<Typography
className={classes.description}
data-cy='project-description'
- >
- {description}
- </Typography>
+ //dangerouslySetInnerHTML is ok here only if description is sanitized,
+ //which it is before it is loaded into the redux store
+ dangerouslySetInnerHTML={{ __html: description }}
+ />
</Collapse>
</section>
</section>
timeout='auto'
collapsedHeight='35px'
>
- <Typography
+ <div
className={classes.description}
data-cy='project-description'
>
<CardContent className={classes.cardContent}>
- <Typography component='div'>
+ <Typography component='div' className={classes.chipSection}>
{Object.keys(currentResource.properties).map((k) =>
Array.isArray(currentResource.properties[k])
? currentResource.properties[k].map((v: string) => getPropertyChip(k, v, undefined, classes.tag))
)}
</Typography>
</CardContent>
- </Typography>
+ </div>
</Collapse>
</section>
</section>