UNPKG

@pnp/spfx-controls-react

Version:

Reusable React controls for SharePoint Framework solutions

596 lines • 25.9 kB
var __extends = (this && this.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; return extendStatics(d, b); }; return function (d, b) { if (typeof b !== "function" && b !== null) throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { if (ar || !(i in from)) { if (!ar) ar = Array.prototype.slice.call(from, 0, i); ar[i] = from[i]; } } return to.concat(ar || Array.prototype.slice.call(from)); }; import * as React from 'react'; import { ScrollablePane, ScrollbarVisibility } from '@fluentui/react/lib/ScrollablePane'; import { Sticky, StickyPositionType } from '@fluentui/react/lib/Sticky'; import { mergeStyleSets } from '@fluentui/react/lib/Styling'; import { DetailsList, DetailsListLayoutMode, Selection, SelectionMode } from '@fluentui/react/lib/DetailsList'; import { GroupOrder } from './IListView'; import { findIndex, has, sortBy, isEqual, cloneDeep } from '@microsoft/sp-lodash-subset'; import { FileTypeIcon, IconType } from '../fileTypeIcon/index'; import * as strings from 'ControlStrings'; import * as telemetry from '../../common/telemetry'; import { DragDropFiles } from "../dragDropFiles"; import filter from 'lodash/filter'; import omit from 'lodash/omit'; import functions from 'lodash/functions'; import { SearchBox } from '@fluentui/react/lib/SearchBox'; import { Guid } from '@microsoft/sp-core-library'; var classNames = mergeStyleSets({ wrapper: { height: '50vh', position: 'relative' } }); /** * Wrap the listview in a scrollable pane if sticky header = true */ var ListViewWrapper = function (_a) { var stickyHeader = _a.stickyHeader, children = _a.children, className = _a.className; return (stickyHeader ? React.createElement("div", { className: "".concat(classNames.wrapper, " ").concat(className !== null && className !== void 0 ? className : "") }, React.createElement(ScrollablePane, { scrollbarVisibility: ScrollbarVisibility.auto }, children)) : React.createElement(React.Fragment, null, children)); }; /** * Lock the searchbox when scrolling if sticky header = true */ var SearchBoxWrapper = function (_a) { var stickyHeader = _a.stickyHeader, children = _a.children; return (stickyHeader ? React.createElement(Sticky, { stickyPosition: StickyPositionType.Header }, children) : React.createElement(React.Fragment, null, children)); }; /** * File type icon component */ var ListView = /** @class */ (function (_super) { __extends(ListView, _super); function ListView(props) { var _this = _super.call(this, props) || this; /** * Check if sorting needs to be set to the column * @param ev * @param column */ _this._columnClick = function (ev, column) { // Find the field in the viewFields list var columnIdx = findIndex(_this.props.viewFields, function (field) { return field.name === column.key; }); // Check if the field has been found if (columnIdx !== -1) { var field = _this.props.viewFields[columnIdx]; // Check if the field needs to be sorted if (has(field, 'sorting')) { // Check if the sorting option is true if (field.sorting) { var sortDescending_1 = typeof column.isSortedDescending === 'undefined' ? false : !column.isSortedDescending; var sortedItems = _this._sortItems(_this.state.items, column.key, sortDescending_1); // Update the columns var sortedColumns = _this.state.columns.map(function (c) { if (c.key === column.key) { c.isSortedDescending = sortDescending_1; c.isSorted = true; } else { c.isSorted = false; c.isSortedDescending = false; } return c; }); // Update the grouping var groupedItems = _this._getGroups(sortedItems, _this.props.groupByFields); // Update the items and columns _this.setState({ items: groupedItems.groups.length > 0 ? groupedItems.items : sortedItems, columns: sortedColumns, groups: groupedItems.groups.length > 0 ? groupedItems.groups : null, }); } } } }; /** * Method updates the controlled value of the filter field * @param newValue */ _this._updateFilterValue = function (filterValue) { var items = cloneDeep(_this.originalItems); var groups = cloneDeep(_this.originalGroups); var columns = cloneDeep(_this.originalColumns); // Check if a value is provided, otherwise revert back to the original list of items if (filterValue && items && items.length > 0) { items = _this._executeFiltering(filterValue, items, columns); var grouping = _this._getGroups(items, _this.props.groupByFields); // Update grouping if (grouping.groups.length > 0) { groups = grouping.groups; // Update the items items = grouping.items; } else { groups = null; } } _this.setState({ filterValue: filterValue, items: items, groups: groups }); }; /** * Custom render of header * @param props * @param defaultRender */ _this._onRenderDetailsHeader = function (props, defaultRender) { if (!props) { return null; } if (_this.props.stickyHeader) { return (React.createElement(Sticky, { stickyPosition: StickyPositionType.Header, isScrollSynced: true }, defaultRender(__assign({}, props)))); } return defaultRender(props); }; telemetry.track('ReactListView', { viewFields: !!props.viewFields, groupByFields: !!props.groupByFields, selectionMode: !!props.selectionMode, selection: !!props.selection, defaultSelection: !!props.defaultSelection }); // Initialize state _this.state = { filterValue: _this.props.defaultFilter }; if (_this.props.selection) { // Initialize the selection _this._selection = new Selection({ // Create the event handler when a selection changes onSelectionChanged: function () { return _this.props.selection(_this._selection.getSelection()); } }); } return _this; } /** * Lifecycle hook when component is mounted */ ListView.prototype.componentDidMount = function () { this._processProperties(this.props); }; /** * Lifecycle hook when component did update after state or property changes * @param prevProps * @param prevState */ ListView.prototype.UNSAFE_componentWillReceiveProps = function (nextProps) { var modifiedNextProps = this._filterFunctions(nextProps); var modifiedProps = this._filterFunctions(this.props); if (!isEqual(modifiedProps, modifiedNextProps)) { // Reset the selected items if (this._selection) { if (!isEqual(modifiedNextProps.items, modifiedProps.items)) { this._selection.setItems(nextProps.items, true); } if (!isEqual(modifiedNextProps.defaultSelection, modifiedProps.defaultSelection)) { this._selection.setAllSelected(false); // select default items this._setSelectedItems(nextProps); } } // Process list view properties this._processProperties(nextProps); } }; /** * Select all the items that should be selected by default */ ListView.prototype._setSelectedItems = function (props) { if (props.items && props.items.length > 0 && props.defaultSelection && props.defaultSelection.length > 0) { for (var _i = 0, _a = props.defaultSelection; _i < _a.length; _i++) { var index = _a[_i]; if (index > -1) { this._selection.setIndexSelected(index, true, false); } } } }; /** * Specify result grouping for the list rendering * @param items * @param groupByFields */ ListView.prototype._getGroups = function (items, groupByFields, level, startIndex) { if (level === void 0) { level = 0; } if (startIndex === void 0) { startIndex = 0; } // Group array which stores the configured grouping var groups = []; var updatedItemsOrder = []; // eslint-disable-line @typescript-eslint/no-explicit-any // Check if there are groupby fields set if (groupByFields) { var groupField_1 = groupByFields[level]; // Check if grouping is configured if (groupByFields && groupByFields.length > 0) { // Create grouped items object var groupedItems_1 = {}; items.forEach(function (item) { var groupName = item[groupField_1.name]; // Check if the group name exists if (typeof groupName === "undefined") { // Set the default empty label for the field groupName = strings.ListViewGroupEmptyLabel; } // Check if group name is a number, this can cause sorting issues if (typeof groupName === "number") { groupName = "".concat(groupName, "."); } // Check if current group already exists if (typeof groupedItems_1[groupName] === "undefined") { // Create a new group of items groupedItems_1[groupName] = []; } groupedItems_1[groupName].push(item); }); // Sort the grouped items object by its key var sortedGroups_1 = {}; var groupNames = Object.keys(groupedItems_1); groupNames = groupField_1.order === GroupOrder.ascending ? groupNames.sort() : groupNames.sort().reverse(); groupNames.forEach(function (key) { sortedGroups_1[key] = groupedItems_1[key]; }); // Loop over all the groups for (var groupItems in sortedGroups_1) { if (!Object.prototype.hasOwnProperty.call(sortedGroups_1, groupItems)) { continue; } // Retrieve the total number of items per group var totalItems = groupedItems_1[groupItems].length; // Create the new group var group = { name: groupItems === "undefined" ? strings.ListViewGroupEmptyLabel : groupItems, key: groupItems === "undefined" ? strings.ListViewGroupEmptyLabel : groupItems, startIndex: startIndex, count: totalItems, }; // Check if child grouping available if (groupByFields[level + 1]) { // Get the child groups var subGroup = this._getGroups(groupedItems_1[groupItems], groupByFields, (level + 1), startIndex); subGroup.items.forEach(function (item) { updatedItemsOrder.push(item); }); group.children = subGroup.groups; } else { // Add the items to the updated items order array groupedItems_1[groupItems].forEach(function (item) { updatedItemsOrder.push(item); }); } // Increase the start index for the next group startIndex = startIndex + totalItems; groups.push(group); } } } return { items: updatedItemsOrder, groups: groups }; }; /** * Process all the component properties */ ListView.prototype._processProperties = function (props) { var items = props.items, iconFieldName = props.iconFieldName, viewFields = props.viewFields, groupByFields = props.groupByFields, showFilter = props.showFilter; var tempState = cloneDeep(this.state); var columns = null; // Check if a set of items was provided if (typeof items !== 'undefined' && items !== null) { tempState.items = this._flattenItems(items); } // Check if an icon needs to be shown if (iconFieldName) { if (columns === null) { columns = []; } var iconColumn = this._createIconColumn(iconFieldName); columns.push(iconColumn); } // Check if view fields were provided if (viewFields) { if (columns === null) { columns = []; } columns = this._createColumns(viewFields, columns); } // Add the columns to the temporary state tempState.columns = columns; if (tempState.items) { // Add grouping to the list view var grouping = this._getGroups(tempState.items, groupByFields); if (grouping.groups.length > 0) { tempState.groups = grouping.groups; // Update the items tempState.items = grouping.items; } else { tempState.groups = null; } } // Store the original items and groups objects this.originalItems = tempState.items; this.originalGroups = tempState.groups; this.originalColumns = tempState.columns; // Check if component needs to be filtered var filterValue = this.state.filterValue; if (filterValue && showFilter) { this.setState({ columns: tempState.columns }); this._updateFilterValue(filterValue); } else { // Update the current component state with the new values this.setState(tempState); } }; /** * Flatten all objects in every item * @param items */ ListView.prototype._flattenItems = function (items) { var _this = this; // Flatten items var flattenItems = items.map(function (item) { // Flatten all objects in the item return _this._flattenItem(item); }); return flattenItems; }; /** * Flatten all object in the item * @param item */ ListView.prototype._flattenItem = function (item) { var flatItem = {}; // eslint-disable-line @typescript-eslint/no-explicit-any for (var parentPropName in item) { // Check if property already exists if (!Object.prototype.hasOwnProperty.call(item, parentPropName)) continue; // Check if the property is of type object if ((typeof item[parentPropName]) === 'object') { // Flatten every object var flatObject = this._flattenItem(item[parentPropName]); for (var childPropName in flatObject) { if (!Object.prototype.hasOwnProperty.call(flatObject, childPropName)) continue; flatItem["".concat(parentPropName, ".").concat(childPropName)] = flatObject[childPropName]; } } else { flatItem[parentPropName] = item[parentPropName]; } } if (!flatItem.key) { flatItem.key = flatItem.ID || flatItem.Id; if (!flatItem.key) { flatItem.key = Guid.newGuid().toString(); } } return flatItem; }; /** * Create an icon column rendering * @param iconField */ ListView.prototype._createIconColumn = function (iconFieldName) { return { key: 'fileType', name: 'File Type', iconName: 'Page', isIconOnly: true, fieldName: 'fileType', minWidth: 16, maxWidth: 16, onRender: function (item) { return (React.createElement(FileTypeIcon, { type: IconType.image, path: item[iconFieldName] })); } }; }; /** * Returns required set of columns for the list view * @param viewFields */ ListView.prototype._createColumns = function (viewFields, crntColumns) { var _this = this; viewFields.forEach(function (field) { crntColumns.push({ key: field.name, name: field.displayName || field.name, fieldName: field.name, minWidth: field.minWidth || 50, maxWidth: field.maxWidth, isResizable: field.isResizable, onRender: _this._fieldRender(field), onColumnClick: _this._columnClick }); }); return crntColumns; }; /** * Check how field needs to be rendered * @param field */ ListView.prototype._fieldRender = function (field) { // Check if a render function is specified if (field.render) { return field.render; } // Check if the URL property is specified if (field.linkPropertyName) { return function (item, index, column) { return React.createElement("a", { href: item[field.linkPropertyName] }, item[column.fieldName]); }; } }; /** * Sort the list of items by the clicked column * @param items * @param columnName * @param descending */ ListView.prototype._sortItems = function (items, columnName, descending) { if (descending === void 0) { descending = false; } if (this.props.sortItems) { return this.props.sortItems(items, columnName, descending); } // Sort the items var ascItems = sortBy(items, [columnName]); var sortedItems = descending ? ascItems.reverse() : ascItems; // Return the sorted items list return sortedItems; }; /** * Executes filtering. Method tries to indicate if filtering should be executed on a single or all columns. * @param filterValue * @param items * @param columns */ ListView.prototype._executeFiltering = function (filterValue, items, columns) { var filterSeparator = ":"; var filterColumns = __spreadArray([], columns, true); if (filterValue && filterValue.indexOf(filterSeparator) >= 0) { var columnName_1 = filterValue.split(filterSeparator)[0]; filterValue = filterValue.split(filterSeparator)[1]; filterColumns = filter(columns, function (column) { return column.fieldName === columnName_1 || column.name === columnName_1; }); } return this._getFilteredItems(filterValue, items, filterColumns); }; /** * Execute filtering on the provided data set and columns * @param filterValue * @param items * @param columns */ ListView.prototype._getFilteredItems = function (filterValue, items, columns) { if (!filterValue) { return items; } var result = []; // eslint-disable-line @typescript-eslint/no-explicit-any for (var _i = 0, items_1 = items; _i < items_1.length; _i++) { var item = items_1[_i]; var addItemToResultSet = false; for (var _a = 0, columns_1 = columns; _a < columns_1.length; _a++) { var viewField = columns_1[_a]; if (this._doesPropertyContainsValue(item, viewField.fieldName, filterValue)) { addItemToResultSet = true; break; } if (this._doesPropertyContainsValue(item, viewField.name, filterValue)) { addItemToResultSet = true; break; } } if (addItemToResultSet) { result.push(item); } } return result; }; /** * Check if the item contains property with proper value * @param item * @param property * @param filterValue */ ListView.prototype._doesPropertyContainsValue = function (item, property, filterValue) { var propertyValue = item[property]; var result = false; if (propertyValue) { // Case insensitive result = propertyValue.toString().toLowerCase().indexOf(filterValue.toLowerCase()) >= 0; } return result; }; ListView.prototype._filterFunctions = function (p) { var modifiedProps = omit(p, functions(p)); if (modifiedProps.items) { modifiedProps.items = modifiedProps.items.map(function (i) { return omit(i, functions(i)); }); } if (modifiedProps.viewFields) { modifiedProps.viewFields = modifiedProps.viewFields.map(function (vf) { return omit(vf, functions(vf)); }); } return modifiedProps; }; /** * Default React component render method */ ListView.prototype.render = function () { var _this = this; var groupProps = {}; var _a = this.props, showFilter = _a.showFilter, filterPlaceHolder = _a.filterPlaceHolder, dragDropFiles = _a.dragDropFiles, stickyHeader = _a.stickyHeader, selectionMode = _a.selectionMode, compact = _a.compact, className = _a.className, listClassName = _a.listClassName, onRenderRow = _a.onRenderRow; var _b = this.state, filterValue = _b.filterValue, items = _b.items, columns = _b.columns, groups = _b.groups; // Check if selection mode is single selection, // if that is the case, disable the selection on grouping headers if (this.props.selectionMode === SelectionMode.single) { groupProps = { headerProps: { onToggleSelectGroup: function () { return null; }, onGroupHeaderClick: function () { return null; }, } }; } return (React.createElement(ListViewWrapper, { stickyHeader: !!stickyHeader, className: className }, React.createElement(DragDropFiles, { enable: dragDropFiles, iconName: "BulkUpload", labelMessage: strings.UploadFileHeader, onDrop: dragDropFiles ? function (files) { _this.props.onDrop(files); } : [] }, showFilter && React.createElement(SearchBoxWrapper, { stickyHeader: !!stickyHeader }, React.createElement(SearchBox, { placeholder: filterPlaceHolder || strings.ListViewFilterLabel, onSearch: this._updateFilterValue, onChange: function (e, value) { return _this._updateFilterValue(value); }, value: filterValue })), !!items && React.createElement(DetailsList, { key: "ListViewControl", items: items, columns: columns, groups: groups, selectionMode: selectionMode || SelectionMode.none, selectionPreservedOnEmptyClick: true, selection: this._selection, layoutMode: DetailsListLayoutMode.justified, compact: compact, setKey: 'ListViewControl', groupProps: groupProps, className: listClassName, onRenderDetailsHeader: this._onRenderDetailsHeader, onRenderRow: onRenderRow, componentRef: function (ref) { if (ref) { ref.forceUpdate(); } } })))); }; return ListView; }(React.Component)); export { ListView }; //# sourceMappingURL=ListView.js.map