Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
41 changes: 40 additions & 1 deletion docs/documentation/docs/controls/ListItemAttachments.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# ListItemAttachments control

This control allows you to manage list item attachments, you can add or delete associated attachments. The attachments are listed in tile view.
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.

Here is an example of the control:

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

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

![ListItemAttachments Details List ](../assets/ListItemAttachmentsDetailsList.png)

![ListItemAttachments Details List Compact ](../assets/ListItemAttachmentsDetailsListCompact.png)

## How to use this control in your solutions

- 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.
Expand All @@ -30,6 +34,30 @@ import { ListItemAttachments } from '@pnp/spfx-controls-react/lib/ListItemAttach
disabled={false} />
```

- You can customize the display mode of attachments. Import the `AttachmentsDisplayMode` enum and use it:

```TypeScript
import { ListItemAttachments, AttachmentsDisplayMode } from '@pnp/spfx-controls-react/lib/ListItemAttachments';

// Tiles view (default)
<ListItemAttachments listId='dfa283f4-5faf-4d54-b6b8-5bcaf2725af5'
itemId={1}
context={this.props.context}
displayMode={AttachmentsDisplayMode.Tiles} />

// List view
<ListItemAttachments listId='dfa283f4-5faf-4d54-b6b8-5bcaf2725af5'
itemId={1}
context={this.props.context}
displayMode={AttachmentsDisplayMode.DetailsList} />

// Compact list view
<ListItemAttachments listId='dfa283f4-5faf-4d54-b6b8-5bcaf2725af5'
itemId={1}
context={this.props.context}
displayMode={AttachmentsDisplayMode.DetailsListCompact} />
```

- If you want to use `ListItemAttachments` controls with new form you have to use React.createRef.

Following example will add selected attachments to list item with id = 1
Expand Down Expand Up @@ -61,10 +89,21 @@ The `ListItemAttachments` control can be configured with the following propertie
| webUrl | string | no | URL of the site. By default it uses the current site URL. |
| label | string | no | Main text to display on the placeholder, next to the icon. |
| description | string | no | Description text to display on the placeholder, below the main text and icon. |
| displayMode | AttachmentsDisplayMode | no | Display mode for rendering attachments. Options: `AttachmentsDisplayMode.Tiles` (default), `AttachmentsDisplayMode.DetailsList`, or `AttachmentsDisplayMode.DetailsListCompact`. |
| disabled | boolean | no | Specifies if the control is disabled or not. |
| 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. |
| 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. |

enum `AttachmentsDisplayMode`

Display mode for rendering attachments.

| Value | Description |
| ---- | ---- |
| Tiles | Displays attachments as tiles/thumbnails using DocumentCard components. This is the default mode. |
| DetailsList | Displays attachments in a list format with file type icons, file names, and delete actions. |
| DetailsListCompact | Displays attachments in a compact list format with reduced row height and padding. |

## Usage with DynamicForm

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:
Expand Down
14 changes: 14 additions & 0 deletions src/controls/listItemAttachments/AttachmentsDisplayMode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export enum AttachmentsDisplayMode {
/**
* Display attachments as tiles/thumbnails using DocumentCard
*/
Tiles = 'tiles',
/**
* Display attachments as a list using DetailsList in normal mode
*/
DetailsList = 'list',
/**
* Display attachments as a compact list using DetailsList in compact mode
*/
DetailsListCompact = 'listCompact'
}
5 changes: 5 additions & 0 deletions src/controls/listItemAttachments/IListItemAttachmentsProps.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { BaseComponentContext } from '@microsoft/sp-component-base';
import { AttachmentsDisplayMode } from './AttachmentsDisplayMode';

export interface IListItemAttachmentsProps {
listId: string;
Expand All @@ -16,6 +17,10 @@ export interface IListItemAttachmentsProps {
* Description text to display on the placeholder, below the main text and icon
*/
description?:string;
/**
* Display mode for rendering attachments. Defaults to Tiles.
*/
displayMode?: AttachmentsDisplayMode;
/**
* Callback function to notify parent components when attachments are modified and the item ETag changes
* @param itemData - The updated item data including the new ETag
Expand Down
11 changes: 11 additions & 0 deletions src/controls/listItemAttachments/ListItemAttachments.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,17 @@
background-color: transparent !important;
font-size: 15px;
}

.detailsList {
.detailsListIcon {
vertical-align: middle;
max-height: 16px;
max-width: 16px;
}
.detailsListLink {
padding-top: 2px;
}
}
}

.uploadBar {
Expand Down
179 changes: 150 additions & 29 deletions src/controls/listItemAttachments/ListItemAttachments.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
// Joao Mendes November 2018, SPFx reusable Control ListItemAttachments
import * as React from 'react';
import { Dialog, DialogType, DialogFooter } from '@fluentui/react/lib/Dialog';
import { PrimaryButton, DefaultButton } from '@fluentui/react/lib/Button';
import { PrimaryButton, DefaultButton, IconButton } from '@fluentui/react/lib/Button';
import { DirectionalHint } from '@fluentui/react/lib/Callout';
import { Label } from "@fluentui/react/lib/Label";
import { Link } from '@fluentui/react/lib/Link';
import { DetailsList, DetailsListLayoutMode, SelectionMode } from '@fluentui/react/lib/DetailsList';
import * as strings from 'ControlStrings';
import styles from './ListItemAttachments.module.scss';
import { UploadAttachment } from './UploadAttachment';
Expand All @@ -17,6 +19,7 @@ import {
import { ImageFit } from '@fluentui/react/lib/Image';
import { IListItemAttachmentsProps } from './IListItemAttachmentsProps';
import { IListItemAttachmentsState } from './IListItemAttachmentsState';
import { AttachmentsDisplayMode } from './AttachmentsDisplayMode';
import SPservice from "../../services/SPService";
import { TooltipHost } from '@fluentui/react/lib/Tooltip';
import { Spinner, SpinnerSize } from '@fluentui/react/lib/Spinner';
Expand Down Expand Up @@ -251,35 +254,26 @@ export class ListItemAttachments extends React.Component<IListItemAttachmentsPro
}

/**
* Default React render method
* Get file extension from filename
* @param fileName - The file name to extract extension from
* @returns The file extension (without the dot) or empty string if no extension
*/
public render(): React.ReactElement<IListItemAttachmentsProps> {
const { openAttachmentsInNewWindow } = this.props;
return (
<div className={styles.ListItemAttachments}>
<UploadAttachment
listId={this.props.listId}
itemId={this.state.itemId}
disabled={this.props.disabled}
context={this.props.context}
onAttachmentUpload={this._onAttachmentUpload}
fireUpload={this.state.fireUpload}
onUploadDialogClosed={() => this.setState({ fireUpload: false })}
onAttachmentChange={this.props.onAttachmentChange}
/>

{
this.state.showPlaceHolder ?
<Placeholder
iconName='Upload'
iconText={this.props.label || strings.ListItemAttachmentslPlaceHolderIconText}
description={this.props.description || strings.ListItemAttachmentslPlaceHolderDescription}
buttonLabel={strings.ListItemAttachmentslPlaceHolderButtonLabel}
hideButton={this.props.disabled}
onConfigure={() => this.setState({ fireUpload: true })} />
:
private getFileExtension(fileName: string): string {
const lastDotIndex = fileName.lastIndexOf('.');
if (lastDotIndex === -1 || lastDotIndex === fileName.length - 1) {
return '';
}
return fileName.substring(lastDotIndex + 1).toLowerCase();
}

this.state.attachments.map(file => {
/**
* Renders attachments in tile/thumbnail mode using DocumentCard components
* @returns JSX element containing attachment tiles
*/
private renderTiles (): JSX.Element {
const { openAttachmentsInNewWindow } = this.props;
return <React.Fragment>{
this.state.attachments.map(file => {
const fileName = file.FileName;
const previewImage = this.previewImages[fileName];
const clickDisabled = !this.state.itemId;
Expand Down Expand Up @@ -321,7 +315,134 @@ export class ListItemAttachments extends React.Component<IListItemAttachmentsPro
</TooltipHost>
</div>
);
})}
})
}</React.Fragment>
}

/**
* Renders attachments in list mode using DetailsList component
* Supports both normal and compact display modes
* @returns JSX element containing attachment list
*/
private renderDetailsList (): JSX.Element {
const { displayMode, openAttachmentsInNewWindow } = this.props;
const columns = [
{
key: 'columnFileType',
name: 'File Type',
iconName: 'Page',
isIconOnly: true,
minWidth: 16,
maxWidth: 16,
onRender: (file: IListItemAttachmentFile) => {
const fileExtension = this.getFileExtension(file.FileName);
const previewImage = this.previewImages[file.FileName];
const iconUrl = previewImage?.previewImageSrc || '';
return (
<TooltipHost content={`${fileExtension || 'file'}`}>
<img src={iconUrl} className={styles.detailsListIcon} alt={`${fileExtension} file icon`} />
</TooltipHost>
);
},
},
{
key: 'columnFileName',
name: 'File Name',
fieldName: 'FileName',
minWidth: 150,
maxWidth: 800,
isResizable: true,
onRender: (file: IListItemAttachmentFile) => {
const clickDisabled = !this.state.itemId;

if (clickDisabled) {
return <span>{file.FileName}</span>;
}

if (openAttachmentsInNewWindow) {
return (
<Link
className={styles.detailsListLink}
onClick={() => window.open(`${file.ServerRelativeUrl}?web=1`, "_blank")}
>
{file.FileName}
</Link>
);
}

return (
<Link className={styles.detailsListLink} href={`${file.ServerRelativeUrl}?web=1`}>
{file.FileName}
</Link>
);
}
},
{
key: 'columnDeleteIcon',
name: '',
minWidth: 32,
maxWidth: 32,
isResizable: true,
onRender: (file: IListItemAttachmentFile) => {
return (
<IconButton
className={styles.detailsListIcon}
iconProps={{ iconName: "Delete" }}
disabled={this.props.disabled}
onClick={
(ev) => {
ev.preventDefault();
ev.stopPropagation();
this.onDeleteAttachment(file); }} />

);
},
}
];
return <DetailsList
className={styles.detailsList}
items={this.state.attachments}
columns={columns}
selectionMode={SelectionMode.none}
layoutMode={DetailsListLayoutMode.justified}
compact={displayMode === AttachmentsDisplayMode.DetailsListCompact}
/>
}

/**
* Default React render method
*/
public render(): React.ReactElement<IListItemAttachmentsProps> {
const { displayMode } = this.props;
return (
<div className={styles.ListItemAttachments}>
<UploadAttachment
listId={this.props.listId}
itemId={this.state.itemId}
disabled={this.props.disabled}
context={this.props.context}
onAttachmentUpload={this._onAttachmentUpload}
fireUpload={this.state.fireUpload}
onUploadDialogClosed={() => this.setState({ fireUpload: false })}
onAttachmentChange={this.props.onAttachmentChange}
/>

{
this.state.showPlaceHolder ?
<Placeholder
iconName='Upload'
iconText={this.props.label || strings.ListItemAttachmentslPlaceHolderIconText}
description={this.props.description || strings.ListItemAttachmentslPlaceHolderDescription}
buttonLabel={strings.ListItemAttachmentslPlaceHolderButtonLabel}
hideButton={this.props.disabled}
onConfigure={() => this.setState({ fireUpload: true })} />
:

<>
{(!displayMode || displayMode === AttachmentsDisplayMode.Tiles) && this.renderTiles()}
{(displayMode === AttachmentsDisplayMode.DetailsList || displayMode === AttachmentsDisplayMode.DetailsListCompact) && this.renderDetailsList()}
</>
}
{!this.state.hideDialog &&

<Dialog
Expand Down
1 change: 1 addition & 0 deletions src/controls/listItemAttachments/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ export * from './IListItemAttachmentsState';
export * from './IUploadAttachmentProps';
export * from './IUploadAttachmentState';
export * from './IListItemAttachmentFile';
export * from './AttachmentsDisplayMode';
export * from './utilities';
export * from './ListItemAttachments';