Skip to content

Commit e54c836

Browse files
authored
Merge pull request #2081 from Ateina/249-ListItemAttachments-customize-render-options
Add render options for ListItemAttachments. Closes #249
2 parents 6626447 + 11fcf96 commit e54c836

8 files changed

Lines changed: 221 additions & 30 deletions

File tree

25.9 KB
Loading
25.1 KB
Loading

docs/documentation/docs/controls/ListItemAttachments.md

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# ListItemAttachments control
22

3-
This control allows you to manage list item attachments, you can add or delete associated attachments. The attachments are listed in tile view.
3+
This control allows you to manage list item attachments, you can add or delete associated attachments. The attachments can be displayed in different modes: tiles (default), list, or compact list.
44

55
Here is an example of the control:
66

@@ -12,6 +12,10 @@ Here is an example of the control:
1212

1313
![ListItemAttachments Attachment Deleted ](../assets/ListItemAttachementDeletedMsg.png)
1414

15+
![ListItemAttachments Details List ](../assets/ListItemAttachmentsDetailsList.png)
16+
17+
![ListItemAttachments Details List Compact ](../assets/ListItemAttachmentsDetailsListCompact.png)
18+
1519
## How to use this control in your solutions
1620

1721
- Check that you installed the `@pnp/spfx-controls-react` dependency. Check out the [getting started](../../#getting-started) page for more information about installing the dependency.
@@ -30,6 +34,30 @@ import { ListItemAttachments } from '@pnp/spfx-controls-react/lib/ListItemAttach
3034
disabled={false} />
3135
```
3236

37+
- You can customize the display mode of attachments. Import the `AttachmentsDisplayMode` enum and use it:
38+
39+
```TypeScript
40+
import { ListItemAttachments, AttachmentsDisplayMode } from '@pnp/spfx-controls-react/lib/ListItemAttachments';
41+
42+
// Tiles view (default)
43+
<ListItemAttachments listId='dfa283f4-5faf-4d54-b6b8-5bcaf2725af5'
44+
itemId={1}
45+
context={this.props.context}
46+
displayMode={AttachmentsDisplayMode.Tiles} />
47+
48+
// List view
49+
<ListItemAttachments listId='dfa283f4-5faf-4d54-b6b8-5bcaf2725af5'
50+
itemId={1}
51+
context={this.props.context}
52+
displayMode={AttachmentsDisplayMode.DetailsList} />
53+
54+
// Compact list view
55+
<ListItemAttachments listId='dfa283f4-5faf-4d54-b6b8-5bcaf2725af5'
56+
itemId={1}
57+
context={this.props.context}
58+
displayMode={AttachmentsDisplayMode.DetailsListCompact} />
59+
```
60+
3361
- If you want to use `ListItemAttachments` controls with new form you have to use React.createRef.
3462

3563
Following example will add selected attachments to list item with id = 1
@@ -61,10 +89,21 @@ The `ListItemAttachments` control can be configured with the following propertie
6189
| webUrl | string | no | URL of the site. By default it uses the current site URL. |
6290
| label | string | no | Main text to display on the placeholder, next to the icon. |
6391
| description | string | no | Description text to display on the placeholder, below the main text and icon. |
92+
| displayMode | AttachmentsDisplayMode | no | Display mode for rendering attachments. Options: `AttachmentsDisplayMode.Tiles` (default), `AttachmentsDisplayMode.DetailsList`, or `AttachmentsDisplayMode.DetailsListCompact`. |
6493
| disabled | boolean | no | Specifies if the control is disabled or not. |
6594
| openAttachmentsInNewWindow | boolean | no | Specifies if the attachment should be opened in a separate browser tab. Use this property set to `true` if you plan to use the component in Microsoft Teams. |
6695
| onAttachmentChange | (itemData: any) => void | no | Callback function invoked when attachments are added or removed. Receives the updated item data including the new ETag. This is useful when using the control within a form (like DynamicForm) that tracks ETags for optimistic concurrency control. |
6796

97+
enum `AttachmentsDisplayMode`
98+
99+
Display mode for rendering attachments.
100+
101+
| Value | Description |
102+
| ---- | ---- |
103+
| Tiles | Displays attachments as tiles/thumbnails using DocumentCard components. This is the default mode. |
104+
| DetailsList | Displays attachments in a list format with file type icons, file names, and delete actions. |
105+
| DetailsListCompact | Displays attachments in a compact list format with reduced row height and padding. |
106+
68107
## Usage with DynamicForm
69108

70109
When using `ListItemAttachments` within a `DynamicForm` or any component that uses ETags for optimistic concurrency control, you should use the `onAttachmentChange` callback to update the ETag when attachments are modified:
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: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,17 @@
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+
.detailsListLink {
44+
padding-top: 2px;
45+
}
46+
}
3647
}
3748

3849
.uploadBar {

src/controls/listItemAttachments/ListItemAttachments.tsx

Lines changed: 150 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,134 @@ 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+
className={styles.detailsListLink}
366+
onClick={() => window.open(`${file.ServerRelativeUrl}?web=1`, "_blank")}
367+
>
368+
{file.FileName}
369+
</Link>
370+
);
371+
}
372+
373+
return (
374+
<Link className={styles.detailsListLink} href={`${file.ServerRelativeUrl}?web=1`}>
375+
{file.FileName}
376+
</Link>
377+
);
378+
}
379+
},
380+
{
381+
key: 'columnDeleteIcon',
382+
name: '',
383+
minWidth: 32,
384+
maxWidth: 32,
385+
isResizable: true,
386+
onRender: (file: IListItemAttachmentFile) => {
387+
return (
388+
<IconButton
389+
className={styles.detailsListIcon}
390+
iconProps={{ iconName: "Delete" }}
391+
disabled={this.props.disabled}
392+
onClick={
393+
(ev) => {
394+
ev.preventDefault();
395+
ev.stopPropagation();
396+
this.onDeleteAttachment(file); }} />
397+
398+
);
399+
},
400+
}
401+
];
402+
return <DetailsList
403+
className={styles.detailsList}
404+
items={this.state.attachments}
405+
columns={columns}
406+
selectionMode={SelectionMode.none}
407+
layoutMode={DetailsListLayoutMode.justified}
408+
compact={displayMode === AttachmentsDisplayMode.DetailsListCompact}
409+
/>
410+
}
411+
412+
/**
413+
* Default React render method
414+
*/
415+
public render(): React.ReactElement<IListItemAttachmentsProps> {
416+
const { displayMode } = this.props;
417+
return (
418+
<div className={styles.ListItemAttachments}>
419+
<UploadAttachment
420+
listId={this.props.listId}
421+
itemId={this.state.itemId}
422+
disabled={this.props.disabled}
423+
context={this.props.context}
424+
onAttachmentUpload={this._onAttachmentUpload}
425+
fireUpload={this.state.fireUpload}
426+
onUploadDialogClosed={() => this.setState({ fireUpload: false })}
427+
onAttachmentChange={this.props.onAttachmentChange}
428+
/>
429+
430+
{
431+
this.state.showPlaceHolder ?
432+
<Placeholder
433+
iconName='Upload'
434+
iconText={this.props.label || strings.ListItemAttachmentslPlaceHolderIconText}
435+
description={this.props.description || strings.ListItemAttachmentslPlaceHolderDescription}
436+
buttonLabel={strings.ListItemAttachmentslPlaceHolderButtonLabel}
437+
hideButton={this.props.disabled}
438+
onConfigure={() => this.setState({ fireUpload: true })} />
439+
:
440+
441+
<>
442+
{(!displayMode || displayMode === AttachmentsDisplayMode.Tiles) && this.renderTiles()}
443+
{(displayMode === AttachmentsDisplayMode.DetailsList || displayMode === AttachmentsDisplayMode.DetailsListCompact) && this.renderDetailsList()}
444+
</>
445+
}
325446
{!this.state.hideDialog &&
326447

327448
<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)