Skip to content

Commit 35fcdee

Browse files
committed
feat(ListItemAttachments): add display mode prop for Tiles and DetailsList views
1 parent 6626447 commit 35fcdee

5 files changed

Lines changed: 177 additions & 29 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export enum AttachmentsDisplayMode {
2+
/**
3+
* Display attachments as tiles/thumbnails using DocumentCard
4+
*/
5+
Tiles = 'tiles',
6+
/**
7+
* Display attachments as a list using DetailsList in normal mode
8+
*/
9+
DetailsList = 'list',
10+
/**
11+
* Display attachments as a compact list using DetailsList in compact mode
12+
*/
13+
DetailsListCompact = 'listCompact'
14+
}

src/controls/listItemAttachments/IListItemAttachmentsProps.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { BaseComponentContext } from '@microsoft/sp-component-base';
2+
import { AttachmentsDisplayMode } from './AttachmentsDisplayMode';
23

34
export interface IListItemAttachmentsProps {
45
listId: string;
@@ -16,6 +17,10 @@ export interface IListItemAttachmentsProps {
1617
* Description text to display on the placeholder, below the main text and icon
1718
*/
1819
description?:string;
20+
/**
21+
* Display mode for rendering attachments. Defaults to Tiles.
22+
*/
23+
displayMode?: AttachmentsDisplayMode;
1924
/**
2025
* Callback function to notify parent components when attachments are modified and the item ETag changes
2126
* @param itemData - The updated item data including the new ETag

src/controls/listItemAttachments/ListItemAttachments.module.scss

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,14 @@
3333
background-color: transparent !important;
3434
font-size: 15px;
3535
}
36+
37+
.detailsList {
38+
.detailsListIcon {
39+
vertical-align: middle;
40+
max-height: 16px;
41+
max-width: 16px;
42+
}
43+
}
3644
}
3745

3846
.uploadBar {

src/controls/listItemAttachments/ListItemAttachments.tsx

Lines changed: 149 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
// Joao Mendes November 2018, SPFx reusable Control ListItemAttachments
22
import * as React from 'react';
33
import { Dialog, DialogType, DialogFooter } from '@fluentui/react/lib/Dialog';
4-
import { PrimaryButton, DefaultButton } from '@fluentui/react/lib/Button';
4+
import { PrimaryButton, DefaultButton, IconButton } from '@fluentui/react/lib/Button';
55
import { DirectionalHint } from '@fluentui/react/lib/Callout';
66
import { Label } from "@fluentui/react/lib/Label";
7+
import { Link } from '@fluentui/react/lib/Link';
8+
import { DetailsList, DetailsListLayoutMode, SelectionMode } from '@fluentui/react/lib/DetailsList';
79
import * as strings from 'ControlStrings';
810
import styles from './ListItemAttachments.module.scss';
911
import { UploadAttachment } from './UploadAttachment';
@@ -17,6 +19,7 @@ import {
1719
import { ImageFit } from '@fluentui/react/lib/Image';
1820
import { IListItemAttachmentsProps } from './IListItemAttachmentsProps';
1921
import { IListItemAttachmentsState } from './IListItemAttachmentsState';
22+
import { AttachmentsDisplayMode } from './AttachmentsDisplayMode';
2023
import SPservice from "../../services/SPService";
2124
import { TooltipHost } from '@fluentui/react/lib/Tooltip';
2225
import { Spinner, SpinnerSize } from '@fluentui/react/lib/Spinner';
@@ -251,35 +254,26 @@ export class ListItemAttachments extends React.Component<IListItemAttachmentsPro
251254
}
252255

253256
/**
254-
* Default React render method
257+
* Get file extension from filename
258+
* @param fileName - The file name to extract extension from
259+
* @returns The file extension (without the dot) or empty string if no extension
255260
*/
256-
public render(): React.ReactElement<IListItemAttachmentsProps> {
257-
const { openAttachmentsInNewWindow } = this.props;
258-
return (
259-
<div className={styles.ListItemAttachments}>
260-
<UploadAttachment
261-
listId={this.props.listId}
262-
itemId={this.state.itemId}
263-
disabled={this.props.disabled}
264-
context={this.props.context}
265-
onAttachmentUpload={this._onAttachmentUpload}
266-
fireUpload={this.state.fireUpload}
267-
onUploadDialogClosed={() => this.setState({ fireUpload: false })}
268-
onAttachmentChange={this.props.onAttachmentChange}
269-
/>
270-
271-
{
272-
this.state.showPlaceHolder ?
273-
<Placeholder
274-
iconName='Upload'
275-
iconText={this.props.label || strings.ListItemAttachmentslPlaceHolderIconText}
276-
description={this.props.description || strings.ListItemAttachmentslPlaceHolderDescription}
277-
buttonLabel={strings.ListItemAttachmentslPlaceHolderButtonLabel}
278-
hideButton={this.props.disabled}
279-
onConfigure={() => this.setState({ fireUpload: true })} />
280-
:
261+
private getFileExtension(fileName: string): string {
262+
const lastDotIndex = fileName.lastIndexOf('.');
263+
if (lastDotIndex === -1 || lastDotIndex === fileName.length - 1) {
264+
return '';
265+
}
266+
return fileName.substring(lastDotIndex + 1).toLowerCase();
267+
}
281268

282-
this.state.attachments.map(file => {
269+
/**
270+
* Renders attachments in tile/thumbnail mode using DocumentCard components
271+
* @returns JSX element containing attachment tiles
272+
*/
273+
private renderTiles (): JSX.Element {
274+
const { openAttachmentsInNewWindow } = this.props;
275+
return <React.Fragment>{
276+
this.state.attachments.map(file => {
283277
const fileName = file.FileName;
284278
const previewImage = this.previewImages[fileName];
285279
const clickDisabled = !this.state.itemId;
@@ -321,7 +315,133 @@ export class ListItemAttachments extends React.Component<IListItemAttachmentsPro
321315
</TooltipHost>
322316
</div>
323317
);
324-
})}
318+
})
319+
}</React.Fragment>
320+
}
321+
322+
/**
323+
* Renders attachments in list mode using DetailsList component
324+
* Supports both normal and compact display modes
325+
* @returns JSX element containing attachment list
326+
*/
327+
private renderDetailsList (): JSX.Element {
328+
const { displayMode, openAttachmentsInNewWindow } = this.props;
329+
const columns = [
330+
{
331+
key: 'columnFileType',
332+
name: 'File Type',
333+
iconName: 'Page',
334+
isIconOnly: true,
335+
minWidth: 16,
336+
maxWidth: 16,
337+
onRender: (file: IListItemAttachmentFile) => {
338+
const fileExtension = this.getFileExtension(file.FileName);
339+
const previewImage = this.previewImages[file.FileName];
340+
const iconUrl = previewImage?.previewImageSrc || '';
341+
return (
342+
<TooltipHost content={`${fileExtension || 'file'}`}>
343+
<img src={iconUrl} className={styles.detailsListIcon} alt={`${fileExtension} file icon`} />
344+
</TooltipHost>
345+
);
346+
},
347+
},
348+
{
349+
key: 'columnFileName',
350+
name: 'File Name',
351+
fieldName: 'FileName',
352+
minWidth: 150,
353+
maxWidth: 800,
354+
isResizable: true,
355+
onRender: (file: IListItemAttachmentFile) => {
356+
const clickDisabled = !this.state.itemId;
357+
358+
if (clickDisabled) {
359+
return <span>{file.FileName}</span>;
360+
}
361+
362+
if (openAttachmentsInNewWindow) {
363+
return (
364+
<Link
365+
onClick={() => window.open(`${file.ServerRelativeUrl}?web=1`, "_blank")}
366+
>
367+
{file.FileName}
368+
</Link>
369+
);
370+
}
371+
372+
return (
373+
<Link href={`${file.ServerRelativeUrl}?web=1`}>
374+
{file.FileName}
375+
</Link>
376+
);
377+
}
378+
},
379+
{
380+
key: 'columnDeleteIcon',
381+
name: '',
382+
minWidth: 32,
383+
maxWidth: 32,
384+
isResizable: true,
385+
onRender: (file: IListItemAttachmentFile) => {
386+
return (
387+
<IconButton
388+
className={styles.detailsListIcon}
389+
iconProps={{ iconName: "Delete" }}
390+
disabled={this.props.disabled}
391+
onClick={
392+
(ev) => {
393+
ev.preventDefault();
394+
ev.stopPropagation();
395+
this.onDeleteAttachment(file); }} />
396+
397+
);
398+
},
399+
}
400+
];
401+
return <DetailsList
402+
className={styles.detailsList}
403+
items={this.state.attachments}
404+
columns={columns}
405+
selectionMode={SelectionMode.none}
406+
layoutMode={DetailsListLayoutMode.justified}
407+
compact={displayMode === AttachmentsDisplayMode.DetailsListCompact}
408+
/>
409+
}
410+
411+
/**
412+
* Default React render method
413+
*/
414+
public render(): React.ReactElement<IListItemAttachmentsProps> {
415+
const { displayMode } = this.props;
416+
return (
417+
<div className={styles.ListItemAttachments}>
418+
<UploadAttachment
419+
listId={this.props.listId}
420+
itemId={this.state.itemId}
421+
disabled={this.props.disabled}
422+
context={this.props.context}
423+
onAttachmentUpload={this._onAttachmentUpload}
424+
fireUpload={this.state.fireUpload}
425+
onUploadDialogClosed={() => this.setState({ fireUpload: false })}
426+
onAttachmentChange={this.props.onAttachmentChange}
427+
/>
428+
429+
{
430+
this.state.showPlaceHolder ?
431+
<Placeholder
432+
iconName='Upload'
433+
iconText={this.props.label || strings.ListItemAttachmentslPlaceHolderIconText}
434+
description={this.props.description || strings.ListItemAttachmentslPlaceHolderDescription}
435+
buttonLabel={strings.ListItemAttachmentslPlaceHolderButtonLabel}
436+
hideButton={this.props.disabled}
437+
onConfigure={() => this.setState({ fireUpload: true })} />
438+
:
439+
440+
<>
441+
{(!displayMode || displayMode === AttachmentsDisplayMode.Tiles) && this.renderTiles()}
442+
{(displayMode === AttachmentsDisplayMode.DetailsList || displayMode === AttachmentsDisplayMode.DetailsListCompact) && this.renderDetailsList()}
443+
</>
444+
}
325445
{!this.state.hideDialog &&
326446

327447
<Dialog

src/controls/listItemAttachments/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ export * from './IListItemAttachmentsState';
44
export * from './IUploadAttachmentProps';
55
export * from './IUploadAttachmentState';
66
export * from './IListItemAttachmentFile';
7+
export * from './AttachmentsDisplayMode';
78
export * from './utilities';
89
export * from './ListItemAttachments';

0 commit comments

Comments
 (0)