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