@pnp/spfx-property-controls
Version:
Reusable property pane controls for SharePoint Framework solutions
454 lines • 22.4 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import * as React from 'react';
import { GeneralHelper } from '../../../../../helpers/GeneralHelper';
import { LoadingState } from './IFileBrowserState';
import { TilesList } from '../TilesList/TilesList';
import { Spinner } from '@fluentui/react/lib/Spinner';
import { DetailsList, DetailsListLayoutMode, Selection, SelectionMode, DetailsRow } from '@fluentui/react/lib/DetailsList';
import { CommandBar } from '@fluentui/react/lib/CommandBar';
import { ScrollablePane } from '@fluentui/react/lib/ScrollablePane';
import styles from './FileBrowser.module.scss';
import * as strings from 'PropertyControlStrings';
const LAYOUT_STORAGE_KEY = 'comparerSiteFilesLayout';
export class FileBrowser extends React.Component {
constructor(props) {
super(props);
/**
* Triggers paged data load
*/
this._loadNextDataRequest = () => __awaiter(this, void 0, void 0, function* () {
if (this.state.loadingState === LoadingState.idle) {
// Load next list items from next page
yield this._getListItems(true);
}
});
/**
* Renders a placeholder to indicate that the folder is empty
*/
this._renderEmptyFolder = () => {
return (React.createElement("div", { className: styles.emptyFolder },
React.createElement("div", { className: styles.emptyFolderImage },
React.createElement("img", { className: styles.emptyFolderImageTag, src: strings.OneDriveEmptyFolderIconUrl, alt: strings.OneDriveEmptyFolderAlt })),
React.createElement("div", { role: "alert" },
React.createElement("div", { className: styles.emptyFolderTitle }, strings.OneDriveEmptyFolderTitle),
React.createElement("div", { className: styles.emptyFolderSubText },
React.createElement("span", { className: styles.emptyFolderPc }, strings.OneDriveEmptyFolderDescription)))));
};
/**
* Renders row with file or folder style.
*/
this._onRenderRow = (props) => {
const fileItem = props.item;
return React.createElement(DetailsRow, Object.assign({}, props, { className: fileItem.isFolder ? styles.folderRow : styles.fileRow }));
};
/**
* Get the list of toolbar items on the left side of the toolbar.
* We leave it empty for now, but we may add the ability to upload later.
*/
this._getToolbarItems = () => {
return [];
};
this.getFarItems = () => {
const { selectedView } = this.state;
let viewIconName = undefined;
let viewName = undefined;
switch (this.state.selectedView) {
case 'list':
viewIconName = 'List';
viewName = strings.ListLayoutList;
break;
case 'compact':
viewIconName = 'AlignLeft';
viewName = strings.ListLayoutCompact;
break;
default:
viewIconName = 'GridViewMedium';
viewName = strings.ListLayoutTile;
}
const farItems = [
{
key: 'listOptions',
className: styles.commandBarNoChevron,
title: strings.ListOptionsTitle,
ariaLabel: strings.ListOptionsAlt.replace('{0}', viewName),
iconProps: {
iconName: viewIconName
},
iconOnly: true,
subMenuProps: {
items: [
{
key: 'list',
name: strings.ListLayoutList,
iconProps: {
iconName: 'List'
},
canCheck: true,
checked: this.state.selectedView === 'list',
ariaLabel: strings.ListLayoutAriaLabel.replace('{0}', strings.ListLayoutList).replace('{1}', selectedView === 'list' ? strings.Selected : undefined),
title: strings.ListLayoutListDescrition,
onClick: (_ev, item) => this._handleSwitchLayout(item)
},
{
key: 'compact',
name: strings.ListLayoutCompact,
iconProps: {
iconName: 'AlignLeft'
},
canCheck: true,
checked: this.state.selectedView === 'compact',
ariaLabel: strings.ListLayoutAriaLabel.replace('{0}', strings.ListLayoutCompact).replace('{1}', selectedView === 'compact' ? strings.Selected : undefined),
title: strings.ListLayoutCompactDescription,
onClick: (_ev, item) => this._handleSwitchLayout(item)
},
{
key: 'tiles',
name: 'Tiles',
iconProps: {
iconName: 'GridViewMedium'
},
canCheck: true,
checked: this.state.selectedView === 'tiles',
ariaLabel: strings.ListLayoutAriaLabel.replace('{0}', strings.ListLayoutTile).replace('{1}', selectedView === 'tiles' ? strings.Selected : undefined),
title: strings.ListLayoutTileDescription,
onClick: (_ev, item) => this._handleSwitchLayout(item)
}
]
}
}
];
return farItems;
};
/**
* Called when users switch the view
*/
this._handleSwitchLayout = (item) => {
if (item) {
// Store the user's favourite layout
if (localStorage) {
localStorage.setItem(LAYOUT_STORAGE_KEY, item.key);
}
this.setState({
selectedView: item.key
});
}
};
/**
* Gratuitous sorting
*/
this._onColumnClick = (event, column) => {
const { columns } = this.state;
let { items } = this.state;
let isSortedDescending = column.isSortedDescending;
// If we've sorted this column, flip it.
if (column.isSorted) {
isSortedDescending = !isSortedDescending;
}
const newColumns = columns.map(col => {
col.isSorted = col.key === column.key;
if (col.isSorted) {
col.isSortedDescending = isSortedDescending;
}
return col;
});
if (items[items.length - 1] !== null) // there are no more items to fetch from the server (there is no 'null' placeholder), we can sort client-side
{
// Sort the items.
items = items.concat([]).sort((a, b) => {
let firstValue = a[column.fieldName || ''];
let secondValue = b[column.fieldName || ''];
if (typeof firstValue === 'string') {
firstValue = firstValue.toLocaleLowerCase();
secondValue = secondValue.toLocaleLowerCase();
}
const sortFactor = isSortedDescending ? -1 : 1;
if (firstValue > secondValue)
return 1 * sortFactor;
else if (firstValue < secondValue)
return -1 * sortFactor;
else
return 0;
});
// If the column being sorted is the 'name' column, then keep all the folders together
if (column.fieldName === "name") {
const folders = items.filter(item => item.isFolder);
const files = items.filter(item => !item.isFolder);
items = [
...(isSortedDescending ? files : folders),
...(isSortedDescending ? folders : files),
];
}
// Reset the items and columns to match the state.
this.setState({
items: items,
columns: newColumns,
currentSortColumnName: column.fieldName
});
}
else { // we need to sort server-side
this.setState({
columns: newColumns,
currentSortColumnName: column.fieldName
}, () => {
this._getListItems().then(() => { }).catch(() => { });
});
}
};
/**
* When a folder is opened, calls parent tab to navigate down
*/
this._handleOpenFolder = (item) => {
// De-select the list item that was clicked, the item in the same position
this._selection.setAllSelected(false);
// item in the folder will appear selected
this.setState({
loadingState: LoadingState.loading,
filePickerResult: undefined
}, () => { this.props.onOpenFolder(item); });
};
/**
* Handles selected item change
*/
this._itemSelectionChanged = (item) => {
let selectedItem = null;
// Deselect item
if (item && this.state.filePickerResult && item.absoluteUrl === this.state.filePickerResult.fileAbsoluteUrl) {
this._selection.setAllSelected(false);
selectedItem = null;
}
else if (item) {
const selectedItemIndex = this.state.items.indexOf(item);
this._selection.selectToIndex(selectedItemIndex);
selectedItem = item;
}
let filePickerResult = null;
if (selectedItem && !selectedItem.isFolder) {
filePickerResult = {
fileAbsoluteUrl: selectedItem.absoluteUrl,
fileName: GeneralHelper.getFileNameFromUrl(selectedItem.name),
fileNameWithoutExtension: GeneralHelper.getFileNameWithoutExtension(selectedItem.name),
spItemUrl: selectedItem.spItemUrl,
downloadFileContent: null
};
}
this.props.onChange(filePickerResult);
this.setState({
filePickerResult
});
};
/**
* Handles item click.
*/
this._handleItemInvoked = (item) => {
// If a file is selected, open the library
if (item.isFolder) {
this._handleOpenFolder(item);
}
else {
// Otherwise, remember it was selected
this._itemSelectionChanged(item);
}
};
// If possible, load the user's favourite layout
const lastLayout = localStorage ?
localStorage.getItem(LAYOUT_STORAGE_KEY)
: 'list';
const columns = [
{
key: 'column1',
name: 'Type',
ariaLabel: strings.TypeAriaLabel,
iconName: 'Page',
isIconOnly: true,
fieldName: 'docIcon',
headerClassName: styles.iconColumnHeader,
minWidth: 16,
maxWidth: 16,
onColumnClick: this._onColumnClick,
onRender: (item) => {
const folderIcon = strings.FolderIconUrl;
// TODO: Improve file icon URL
const isPhoto = GeneralHelper.isImage(item.name);
const iconUrl = isPhoto
? strings.PhotoIconUrl
: item.fileType.toLowerCase() === "aspx"
? 'https://res-1.cdn.office.net/files/fabric-cdn-prod_20220127.003/assets/item-types/20/spo.svg'
: `https://res-1.cdn.office.net/files/fabric-cdn-prod_20220127.003/assets/item-types/20/${item.fileType}.svg`;
const altText = item.isFolder ? strings.FolderAltText : strings.ImageAltText.replace('{0}', item.fileType);
return React.createElement("div", { className: styles.fileTypeIcon },
React.createElement("img", { src: item.isFolder ? folderIcon : iconUrl, className: styles.fileTypeIconIcon, alt: altText, title: altText }));
}
},
{
key: 'column2',
name: strings.NameField,
fieldName: 'name',
minWidth: 210,
isRowHeader: true,
isResizable: true,
isSorted: true,
isSortedDescending: false,
sortAscendingAriaLabel: strings.SortedAscending,
sortDescendingAriaLabel: strings.SortedDescending,
onColumnClick: this._onColumnClick,
data: 'string',
isPadded: true,
onRender: (item) => {
if (item.isFolder) {
return React.createElement("span", { className: styles.folderItem, onClick: (_event) => this._handleOpenFolder(item) }, item.name);
}
else {
return React.createElement("span", { className: styles.fileItem }, item.name);
}
},
},
{
key: 'column3',
name: strings.ModifiedField,
fieldName: 'modified',
minWidth: 120,
isResizable: true,
onColumnClick: this._onColumnClick,
data: 'number',
onRender: (item) => {
//const dateModified = moment(item.modified).format(strings.DateFormat);
return React.createElement("span", null, item.modifiedFriendly);
},
isPadded: true
},
{
key: 'column4',
name: strings.ModifiedByField,
fieldName: 'modifiedBy',
minWidth: 120,
isResizable: true,
data: 'string',
onColumnClick: this._onColumnClick,
onRender: (item) => {
return React.createElement("span", null, item.modifiedBy);
},
isPadded: true
},
{
key: 'column5',
name: strings.FileSizeField,
fieldName: 'fileSize',
minWidth: 70,
maxWidth: 90,
isResizable: true,
data: 'number',
onColumnClick: this._onColumnClick,
onRender: (item) => {
return React.createElement("span", null, item.fileSize ? GeneralHelper.formatBytes(item.fileSize, 1) : undefined);
}
}
];
this._selection = new Selection({
selectionMode: SelectionMode.single
});
const currentSortColumn = columns.filter(c => c.isSorted === true); // TODO: switch to '.find' if/when this codebase upgrade to >= ES2015
this.state = {
columns: columns,
items: [],
nextPageQueryString: null,
loadingState: LoadingState.loading,
selectedView: lastLayout,
filePickerResult: null,
currentSortColumnName: currentSortColumn.length ? currentSortColumn[0].fieldName : null,
};
}
/**
* Gets the list of files when settings change
* @param prevProps
* @param prevState
*/
componentDidUpdate(prevProps, prevState) {
if (this.props.folderPath !== prevProps.folderPath) {
this._selection.setAllSelected(false);
this._getListItems().then(() => { }).catch(() => { });
}
}
/**
* Gets the list of files when tab first loads
*/
componentDidMount() {
this._getListItems().then(() => { }).catch(() => { });
}
render() {
return (React.createElement("div", null,
(this.state.items && this.state.items.length > 0 && this.state.loadingState !== LoadingState.loading) &&
React.createElement("div", null,
React.createElement("div", { className: styles.itemPickerTopBar },
React.createElement(CommandBar, { items: this._getToolbarItems(), farItems: this.getFarItems() })),
React.createElement("div", { className: styles.scrollablePaneWrapper },
React.createElement(ScrollablePane, null, this.state.selectedView !== 'tiles' ?
(React.createElement(DetailsList, { items: this.state.items, compact: this.state.selectedView === 'compact', columns: this.state.columns, selectionMode: SelectionMode.single, setKey: "set", layoutMode: DetailsListLayoutMode.justified, isHeaderVisible: true, selection: this._selection, onActiveItemChanged: (item, index, ev) => this._handleItemInvoked(item), selectionPreservedOnEmptyClick: true, enterModalSelectionOnTouch: true, onRenderRow: this._onRenderRow, onRenderMissingItem: () => { this._loadNextDataRequest().then(() => { }).catch(() => { }); return null; } })) :
(React.createElement(TilesList, { fileBrowserService: this.props.fileBrowserService, filePickerResult: this.state.filePickerResult, selection: this._selection, items: this.state.items, onFolderOpen: this._handleOpenFolder, onFileSelected: this._itemSelectionChanged, onNextPageDataRequest: this._loadNextDataRequest, context: this.props.context }))))),
(this.state.loadingState === LoadingState.idle && (!this.state.items || this.state.items.length <= 0)) &&
/* Render information about empty folder */
this._renderEmptyFolder(),
this.state.loadingState !== LoadingState.idle &&
React.createElement(Spinner, { label: strings.Loading })));
}
/**
* Gets all files in a library with a matchihg path
*/
_getListItems(concatenateResults = false) {
return __awaiter(this, void 0, void 0, function* () {
const { libraryId, folderPath, accepts } = this.props;
let { nextPageQueryString } = this.state;
const { items, currentSortColumnName, columns } = this.state;
const currentSortColumn = columns.filter(c => c.fieldName === currentSortColumnName); // TODO: switch to '.find' if/when this codebase upgrade to >= ES2015
const isSortedDescending = currentSortColumn.length ? currentSortColumn[0].isSortedDescending : false;
let filesQueryResult = { items: [], nextHref: null };
const loadingState = concatenateResults ? LoadingState.loadingNextPage : LoadingState.loading;
// If concatenate results is set to false -> it's needed to load new data without nextPageUrl
nextPageQueryString = concatenateResults ? nextPageQueryString : null;
try {
this.setState({
loadingState,
nextPageQueryString
});
// Load files in the folder
filesQueryResult = yield this.props.fileBrowserService.getListItemsByListId(libraryId, folderPath, accepts, nextPageQueryString, currentSortColumnName, isSortedDescending);
}
catch (error) {
filesQueryResult.items = null;
console.error(error.message);
}
finally {
// Remove the null mark from the end of the items array
if (concatenateResults && items && items.length > 0 && items[items.length - 1] === null) {
// Remove the null mark
items.splice(items.length - 1, 1);
}
//concatenate results
const newItems = concatenateResults ? items.concat(filesQueryResult.items) : filesQueryResult.items;
// If there are more items to load -> add null mark at the end of the array
if (filesQueryResult.nextHref) {
newItems.push(null);
}
if (!concatenateResults) {
// de-select anything that was previously selected
this._selection.setAllSelected(false);
}
this.setState({
items: newItems,
nextPageQueryString: filesQueryResult.nextHref,
// isLoading: false,
// isLoadingNextPage: false
loadingState: LoadingState.idle
});
}
});
}
}
//# sourceMappingURL=FileBrowser.js.map