21224: fixed single-line toggle height
[arvados.git] / services / workbench2 / src / views-components / details-card / project-details-card.tsx
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import React from 'react';
6 import { StyleRulesCallback, Card, CardHeader, WithStyles, withStyles, Typography, CardContent, Tooltip, Collapse, Grid } from '@material-ui/core';
7 import { ArvadosTheme } from 'common/custom-theme';
8 import { RootState } from 'store/store';
9 import { connect } from 'react-redux';
10 import { getResource } from 'store/resources/resources';
11 import { getPropertyChip } from '../resource-properties-form/property-chip';
12 import { ProjectResource } from 'models/project';
13 import { FavoriteStar, PublicFavoriteStar } from 'views-components/favorite-star/favorite-star';
14 import { FreezeIcon } from 'components/icon/icon';
15 import { Resource } from 'models/resource';
16 import { Dispatch } from 'redux';
17 import { loadDetailsPanel } from 'store/details-panel/details-panel-action';
18 import { ExpandChevronRight } from 'components/expand-chevron-right/expand-chevron-right';
19 import { MultiselectToolbar } from 'components/multiselect-toolbar/MultiselectToolbar';
20 import { setSelectedResourceUuid } from 'store/selected-resource/selected-resource-actions';
21 import { deselectAllOthers } from 'store/multiselect/multiselect-actions';
22
23 type CssRules =
24     | 'root'
25     | 'cardHeaderContainer'
26     | 'cardHeader'
27     | 'descriptionToggle'
28     | 'showMore'
29     | 'noDescription'
30     | 'userNameContainer'
31     | 'cardContent'
32     | 'nameSection'
33     | 'namePlate'
34     | 'faveIcon'
35     | 'frozenIcon'
36     | 'chipToggle'
37     | 'chipSection'
38     | 'tag'
39     | 'description'
40     | 'toolbarStyles';
41
42 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
43     root: {
44         width: '100%',
45         marginBottom: '1rem',
46         flex: '0 0 auto',
47         padding: 0,
48         minHeight: '3rem',
49     },
50     noDescription: {
51         color: theme.palette.grey['600'],
52         fontStyle: 'italic',
53         marginLeft: '2rem',
54     },
55     userNameContainer: {
56         display: 'flex',
57         alignItems: 'center',
58         minHeight: '2.7rem',
59     },
60     cardHeaderContainer: {
61         width: '100%',
62         display: 'flex',
63         flexDirection: 'row',
64         alignItems: 'center',
65         justifyContent: 'space-between',
66     },
67     cardHeader: {
68         minWidth: '30rem',
69         padding: '0.2rem 0.4rem 0.2rem 1rem',
70     },
71     descriptionToggle: {
72         display: 'flex',
73         flexDirection: 'row',
74         cursor: 'pointer',
75         marginTop: '-0.25rem',
76         paddingBottom: '0.5rem',
77     },
78     cardContent: {
79         display: 'flex',
80         flexDirection: 'column',
81         paddingTop: 0,
82         paddingLeft: '0.1rem',
83     },
84     nameSection: {
85         display: 'flex',
86         flexDirection: 'row',
87         alignItems: 'center',
88     },
89     namePlate: {
90         display: 'flex',
91         flexDirection: 'row',
92         alignItems: 'center',
93         margin: 0,
94         minHeight: '2.7rem',
95     },
96     faveIcon: {
97         fontSize: '0.8rem',
98         margin: 'auto 0 1rem 0.3rem',
99         color: theme.palette.text.primary,
100     },
101     frozenIcon: {
102         fontSize: '0.5rem',
103         marginLeft: '0.3rem',
104         height: '1rem',
105         color: theme.palette.text.primary,
106     },
107     showMore: {
108         marginTop: 0,
109         cursor: 'pointer',
110     },
111     chipToggle: {
112         display: 'flex',
113         alignItems: 'center',
114         height: '2rem',
115     },
116     chipSection: {
117         marginBottom: '-2rem',
118     },
119     tag: {
120         marginRight: '0.75rem',
121         marginBottom: '0.5rem',
122     },
123     description: {
124         marginTop: 0,
125         marginRight: '2rem',
126         marginBottom: '-0.85rem',
127     },
128     toolbarStyles: {
129         marginRight: '-1rem',
130     },
131 });
132
133 const mapStateToProps = ({ auth, selectedResourceUuid, resources, properties }: RootState) => {
134     const currentResource = getResource(properties.currentRouteUuid)(resources);
135     const frozenByUser = currentResource && getResource((currentResource as ProjectResource).frozenByUuid as string)(resources);
136     const frozenByFullName = frozenByUser && (frozenByUser as Resource & { fullName: string }).fullName;
137     const isSelected = selectedResourceUuid === properties.currentRouteUuid;
138
139     return {
140         isAdmin: auth.user?.isAdmin,
141         currentResource,
142         frozenByFullName,
143         isSelected,
144     };
145 };
146
147 const mapDispatchToProps = (dispatch: Dispatch) => ({
148     handleCardClick: (uuid: string) => {
149         dispatch<any>(loadDetailsPanel(uuid));
150         dispatch<any>(setSelectedResourceUuid(uuid));
151         dispatch<any>(deselectAllOthers(uuid));
152     },
153 });
154
155 type ProjectCardProps = WithStyles<CssRules> & {
156     currentResource: ProjectResource;
157     frozenByFullName: string | undefined;
158     isAdmin: boolean;
159     isSelected: boolean;
160     handleCardClick: (resource: any) => void;
161 };
162
163 export const ProjectCard = connect(
164     mapStateToProps,
165     mapDispatchToProps
166 )(
167     withStyles(styles)((props: ProjectCardProps) => {
168         const { classes, currentResource, frozenByFullName, handleCardClick, isSelected } = props;
169         const { name, description, uuid } = currentResource as ProjectResource;
170         const [showDescription, setShowDescription] = React.useState(false);
171         const [showProperties, setShowProperties] = React.useState(false);
172
173         const toggleDescription = () => {
174             setShowDescription(!showDescription);
175         };
176
177         const toggleProperties = () => {
178             setShowProperties(!showProperties);
179         };
180
181         return (
182             <Card
183                 className={classes.root}
184                 onClick={() => handleCardClick(uuid)}
185                 data-cy='project-details-card'
186             >
187                 <Grid
188                     container
189                     wrap='nowrap'
190                     className={classes.cardHeaderContainer}
191                 >
192                     <CardHeader
193                         className={classes.cardHeader}
194                         title={
195                             <section className={classes.nameSection}>
196                                 <section className={classes.namePlate}>
197                                     <Typography
198                                         variant='h6'
199                                         style={{ marginRight: '1rem' }}
200                                     >
201                                         {name}
202                                     </Typography>
203                                     <FavoriteStar
204                                         className={classes.faveIcon}
205                                         resourceUuid={currentResource.uuid}
206                                     />
207                                     <PublicFavoriteStar
208                                         className={classes.faveIcon}
209                                         resourceUuid={currentResource.uuid}
210                                     />
211                                     {!!frozenByFullName && (
212                                         <Tooltip
213                                             className={classes.frozenIcon}
214                                             disableFocusListener
215                                             title={<span>Project was frozen by {frozenByFullName}</span>}
216                                         >
217                                             <FreezeIcon style={{ fontSize: 'inherit' }} />
218                                         </Tooltip>
219                                     )}
220                                 </section>
221                                 {!description && (
222                                     <Typography
223                                         data-cy='no-description'
224                                         className={classes.noDescription}
225                                     >
226                                         no description available
227                                     </Typography>
228                                 )}
229                             </section>
230                         }
231                     />
232                     {isSelected && <MultiselectToolbar injectedStyles={classes.toolbarStyles} />}
233                 </Grid>
234                 <section onClick={(ev) => ev.stopPropagation()}>
235                     {description ? (
236                         <section
237                             onClick={toggleDescription}
238                             className={classes.descriptionToggle}
239                             data-cy='toggle-description'
240                         >
241                             <ExpandChevronRight expanded={showDescription} />
242                             <section className={classes.showMore}>
243                                 <Collapse
244                                     in={showDescription}
245                                     timeout='auto'
246                                     collapsedHeight='1.25rem'
247                                 >
248                                     <Typography
249                                         className={classes.description}
250                                         data-cy='project-description'
251                                         //dangerouslySetInnerHTML is ok here only if description is sanitized,
252                                         //which it is before it is loaded into the redux store
253                                         dangerouslySetInnerHTML={{ __html: description }}
254                                     />
255                                 </Collapse>
256                             </section>
257                         </section>
258                     ) : (
259                         <></>
260                     )}
261                     {typeof currentResource.properties === 'object' && Object.keys(currentResource.properties).length > 0 ? (
262                         <section
263                             onClick={toggleProperties}
264                             className={classes.descriptionToggle}
265                         >
266                             <div
267                                 className={classes.chipToggle}
268                                 data-cy='toggle-chips'
269                             >
270                                 <ExpandChevronRight expanded={showProperties} />
271                             </div>
272                             <section className={classes.showMore}>
273                                 <Collapse
274                                     in={showProperties}
275                                     timeout='auto'
276                                     collapsedHeight='35px'
277                                 >
278                                     <div
279                                         className={classes.description}
280                                         data-cy='project-description'
281                                     >
282                                         <CardContent className={classes.cardContent}>
283                                             <Typography
284                                                 component='div'
285                                                 className={classes.chipSection}
286                                             >
287                                                 {Object.keys(currentResource.properties).map((k) =>
288                                                     Array.isArray(currentResource.properties[k])
289                                                         ? currentResource.properties[k].map((v: string) => getPropertyChip(k, v, undefined, classes.tag))
290                                                         : getPropertyChip(k, currentResource.properties[k], undefined, classes.tag)
291                                                 )}
292                                             </Typography>
293                                         </CardContent>
294                                     </div>
295                                 </Collapse>
296                             </section>
297                         </section>
298                     ) : null}
299                 </section>
300             </Card>
301         );
302     })
303 );