зеркало из
1
0
Форкнуть 0
* make device list simple detailed list with no collapse and wire up command bar

* add max width to properties list

* remove a comment
This commit is contained in:
YingXue 2019-12-10 10:47:59 -08:00 коммит произвёл Paul Montgomery
Родитель 0c2c943a93
Коммит 8e8165ff6a
13 изменённых файлов: 162 добавлений и 1107 удалений

Просмотреть файл

@ -0,0 +1,5 @@
export const MEDIUM_COLUMN_WIDTH = 300;
export const SMALL_COLUMN_WIDTH = 200;
export const EXTRA_SMALL_COLUMN_WIDTH = 100;
export const LARGE_COLUMN_WIDTH = 400;
export const EXTRA_LARGE_COLUMN_WIDTH = 500;

Просмотреть файл

@ -38,4 +38,25 @@
bottom: 0px;
}
}
}
.list-detail {
.ms-DetailsHeader {
@include themify($themes) {
background: themed('cellBackground') !important;
}
}
.ms-DetailsRow-cell {
@include themify($themes) {
background: themed('cellBackground') !important;
}
}
a:link, a:visited, a:hover, a:active {
text-decoration: none;
@include themify($themes) {
color: themed('linkColor');
}
}
}

Просмотреть файл

@ -143,6 +143,7 @@ exports[`devicePropertiesPerInterface matches snapshot 1`] = `
"isMultiline": true,
"isResizable": true,
"key": "name",
"maxWidth": 500,
"minWidth": 100,
"name": "deviceProperties.columns.name",
},
@ -150,6 +151,7 @@ exports[`devicePropertiesPerInterface matches snapshot 1`] = `
"fieldName": "schema",
"isResizable": true,
"key": "schema",
"maxWidth": 200,
"minWidth": 100,
"name": "deviceProperties.columns.schema",
},
@ -157,6 +159,7 @@ exports[`devicePropertiesPerInterface matches snapshot 1`] = `
"fieldName": "unit",
"isResizable": true,
"key": "unit",
"maxWidth": 200,
"minWidth": 100,
"name": "deviceProperties.columns.unit",
},

Просмотреть файл

@ -14,6 +14,7 @@ import ComplexReportedFormPanel from '../shared/complexReportedFormPanel';
import { RenderSimplyTypeValue } from '../shared/simpleReportedSection';
import { PropertyContent } from '../../../../api/models/modelDefinition';
import { ParsedJsonSchema } from '../../../../api/models/interfaceJsonParserOutput';
import { SMALL_COLUMN_WIDTH, EXTRA_LARGE_COLUMN_WIDTH } from '../../../../constants/columnWidth';
export interface DevicePropertiesDataProps {
twinAndSchema: TwinWithSchema[];
@ -67,9 +68,9 @@ export default class DevicePropertiesPerInterface
private readonly getColumns = (context: LocalizationContextInterface): IColumn[] => {
return [
{ key: 'name', name: context.t(ResourceKeys.deviceProperties.columns.name), fieldName: 'name', minWidth: 100, isResizable: true, isMultiline: true },
{ key: 'schema', name: context.t(ResourceKeys.deviceProperties.columns.schema), fieldName: 'schema', minWidth: 100, isResizable: true },
{ key: 'unit', name: context.t(ResourceKeys.deviceProperties.columns.unit), fieldName: 'unit', minWidth: 100, isResizable: true },
{ key: 'name', name: context.t(ResourceKeys.deviceProperties.columns.name), fieldName: 'name', minWidth: 100, maxWidth: EXTRA_LARGE_COLUMN_WIDTH, isResizable: true, isMultiline: true },
{ key: 'schema', name: context.t(ResourceKeys.deviceProperties.columns.schema), fieldName: 'schema', minWidth: 100, maxWidth: SMALL_COLUMN_WIDTH, isResizable: true },
{ key: 'unit', name: context.t(ResourceKeys.deviceProperties.columns.unit), fieldName: 'unit', minWidth: 100, maxWidth: SMALL_COLUMN_WIDTH, isResizable: true },
{ key: 'value', name: context.t(ResourceKeys.deviceProperties.columns.value), fieldName: 'value', minWidth: 150, isResizable: true }
];
}

Просмотреть файл

@ -1,16 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/devices/deviceListCell matches snapshot while loading 1`] = `
<div
className="device-list-cell-container"
data-selection-index={1}
>
<div
className="device-list-cell-container-content"
>
<span>
common.loading
</span>
</div>
</div>
`;

Просмотреть файл

@ -8,18 +8,21 @@ import { Icon } from 'office-ui-fabric-react/lib/Icon';
import { Label } from 'office-ui-fabric-react/lib/Label';
import { Dialog, DialogFooter, DialogType } from 'office-ui-fabric-react/lib/Dialog';
import { PrimaryButton, DefaultButton } from 'office-ui-fabric-react/lib/Button';
import { DetailsList, DetailsListLayoutMode, IColumn, Selection } from 'office-ui-fabric-react/lib/DetailsList';
import { MarqueeSelection } from 'office-ui-fabric-react/lib/MarqueeSelection';
import { Announced } from 'office-ui-fabric-react/lib/Announced';
import { LocalizationContextConsumer, LocalizationContextInterface } from '../../../shared/contexts/localizationContext';
import { ResourceKeys } from '../../../../localization/resourceKeys';
import GroupedListWrapper from '../../../shared/components/groupedList';
import { DeviceSummary } from '../../../api/models/deviceSummary';
import DeviceQuery from '../../../api/models/deviceQuery';
import DeviceListCommandBar from './deviceListCommandBar';
import BreadcrumbContainer from '../../../shared/components/breadcrumbContainer';
import DeviceListQuery from './deviceListQuery';
import { DeviceListCell } from './deviceListCell';
import ListPaging from './listPaging';
import { ROUTE_PARTS, ROUTE_PARAMS } from '../../../constants/routes';
import { CHECK } from '../../../constants/iconNames';
import MultiLineShimmer from '../../../shared/components/multiLineShimmer';
import { LARGE_COLUMN_WIDTH, EXTRA_SMALL_COLUMN_WIDTH, SMALL_COLUMN_WIDTH, MEDIUM_COLUMN_WIDTH } from '../../../constants/columnWidth';
import '../../../css/_deviceList.scss';
import '../../../css/_layouts.scss';
@ -41,6 +44,7 @@ interface DeviceListState {
refreshQuery: number;
}
const SHIMMER_COUNT = 10;
class DeviceListComponent extends React.Component<DeviceListDataProps & DeviceListDispatchProps & RouteComponentProps, DeviceListState> {
constructor(props: DeviceListDataProps & DeviceListDispatchProps & RouteComponentProps) {
super(props);
@ -50,8 +54,18 @@ class DeviceListComponent extends React.Component<DeviceListDataProps & DeviceLi
selectedDeviceIds: [],
showDeleteConfirmation: false
};
this.selection = new Selection({
onSelectionChanged: () => {
this.setState({
selectedDeviceIds: this.selection.getSelection() && this.selection.getSelection().map(selection => (selection as DeviceSummary).deviceId)
});
}
});
}
private selection: Selection;
public render() {
if (!this.props.connectionString) {
return (<Redirect to="/" />);
@ -118,132 +132,129 @@ class DeviceListComponent extends React.Component<DeviceListDataProps & DeviceLi
}
private readonly showDeviceList = (context: LocalizationContextInterface) => {
const renderCell = (nestingDepth: number, item: DeviceSummary, itemIndex: number) => {
return (
<DeviceListCell
connectionString={this.props.connectionString}
device={item}
itemIndex={itemIndex}
/>
);
};
return (
<>
{this.showPaging()}
<div className="list-detail">
{this.props.isFetching ?
<MultiLineShimmer shimmerCount={SHIMMER_COUNT}/> :
(this.props.devices && this.props.devices.length !== 0 ?
<MarqueeSelection selection={this.selection}>
<DetailsList
onRenderItemColumn={this.renderItemColumn(context)}
items={!this.props.isFetching && this.props.devices}
columns={this.getColumns(context)}
layoutMode={DetailsListLayoutMode.justified}
selection={this.selection}
/>
</MarqueeSelection> :
<>
<h3>{context.t(ResourceKeys.deviceLists.noDevice)}</h3>
<Announced
message={context.t(ResourceKeys.deviceLists.noDevice)}
/>
</>
)
}
</div>
</>
);
}
private readonly showPaging = () => {
return (
<ListPaging
continuationTokens={this.props.query && this.props.query.continuationTokens}
currentPageIndex={this.props.query && this.props.query.currentPageIndex}
fetchPage={this.fetchPage}
/>
<GroupedListWrapper
items={this.props.devices}
nameKey="deviceId"
isLoading={this.props.isFetching}
noItemsMessage={context.t(ResourceKeys.deviceLists.noDevice)}
onRenderCell={renderCell}
onSelectionChanged={this.onRowSelection}
columnInfo={[
{
infoText: context.t(ResourceKeys.deviceLists.columns.deviceId.infoText),
name: context.t(ResourceKeys.deviceLists.columns.deviceId.label),
onRenderColumn: (group, key) => {
const path = this.props.location.pathname.replace(/\/devices\/.*/, `/${ROUTE_PARTS.DEVICES}`);
return (
<NavLink key={key} className={'deviceId-label'} to={`${path}/${ROUTE_PARTS.DETAIL}/${ROUTE_PARTS.IDENTITY}/?${ROUTE_PARAMS.DEVICE_ID}=${encodeURIComponent(group.name)}`}>
{group.name}
</NavLink>
);
},
widthPercentage: 20
},
{
infoText: context.t(ResourceKeys.deviceLists.columns.status.infoText),
name: context.t(ResourceKeys.deviceLists.columns.status.label),
onRenderColumn: (group, key) => {
return (
<Label
key={key}
>
{(group.data as DeviceSummary).status}
</Label>
);
},
widthPercentage: 10
},
{
name: context.t(ResourceKeys.deviceLists.columns.connection),
onRenderColumn: (group, key) => {
return (
<Label
key={key}
>
{(group.data as DeviceSummary).connectionState}
</Label>
);
},
widthPercentage: 10
},
{
name: context.t(ResourceKeys.deviceLists.columns.authenticationType),
onRenderColumn: (group, key) => {
return (
<Label
key={key}
>
{(group.data as DeviceSummary).authenticationType}
</Label>
);
},
widthPercentage: 10
},
{
name: context.t(ResourceKeys.deviceLists.columns.lastActivityTime),
onRenderColumn: (group, key) => {
return (
<Label
key={key}
>
{(group.data as DeviceSummary).lastActivityTime || '--'}
</Label>
);
},
widthPercentage: 15
},
{
name: context.t(ResourceKeys.deviceLists.columns.statusUpdatedTime),
onRenderColumn: (group, key) => {
return (
<Label
key={key}
>
{(group.data as DeviceSummary).statusUpdatedTime || '--'}
</Label>
);
},
widthPercentage: 15
},
{
name: context.t(ResourceKeys.deviceLists.columns.isEdgeDevice.label),
onRenderColumn: (group, key) => {
const isEdge = (group.data as DeviceSummary).iotEdge;
return (
<Icon
key={key}
iconName={isEdge && CHECK}
ariaLabel={isEdge ?
context.t(ResourceKeys.deviceLists.columns.isEdgeDevice.yes) : context.t(ResourceKeys.deviceLists.columns.isEdgeDevice.no)}
/>
);
},
widthPercentage: 5
}
]}
/>
</>
);
}
private readonly getColumns = (context: LocalizationContextInterface): IColumn[] => {
return [
{ fieldName: 'id', isMultiline: true, isResizable: true, key: 'id',
maxWidth: LARGE_COLUMN_WIDTH, minWidth: 100, name: context.t(ResourceKeys.deviceLists.columns.deviceId.label) },
{ fieldName: 'status', isResizable: true, key: 'status',
maxWidth: EXTRA_SMALL_COLUMN_WIDTH, minWidth: 100, name: context.t(ResourceKeys.deviceLists.columns.status.label)},
{ fieldName: 'connection', isResizable: true, key: 'connection',
maxWidth: SMALL_COLUMN_WIDTH, minWidth: 100, name: context.t(ResourceKeys.deviceLists.columns.connection) },
{ fieldName: 'authenticationType', isMultiline: true, isResizable: true, key: 'authenticationType',
maxWidth: SMALL_COLUMN_WIDTH, minWidth: 100, name: context.t(ResourceKeys.deviceLists.columns.authenticationType)},
{ fieldName: 'lastActivityTime', isMultiline: true, isResizable: true, key: 'lastActivityTime',
maxWidth: MEDIUM_COLUMN_WIDTH, minWidth: 100, name: context.t(ResourceKeys.deviceLists.columns.lastActivityTime)},
{ fieldName: 'statusUpdatedTime', isMultiline: true, isResizable: true, key: 'statusUpdatedTime',
maxWidth: MEDIUM_COLUMN_WIDTH, minWidth: 100, name: context.t(ResourceKeys.deviceLists.columns.statusUpdatedTime)},
{ fieldName: 'edge', isResizable: true, key: 'edge',
minWidth: 100, name: context.t(ResourceKeys.deviceLists.columns.isEdgeDevice.label)},
];
}
// tslint:disable-next-line:cyclomatic-complexity
private readonly renderItemColumn = (context: LocalizationContextInterface) => (item: DeviceSummary, index: number, column: IColumn) => {
switch (column.key) {
case 'id':
const path = this.props.location.pathname.replace(/\/devices\/.*/, `/${ROUTE_PARTS.DEVICES}`);
return (
<NavLink key={column.key} to={`${path}/${ROUTE_PARTS.DETAIL}/${ROUTE_PARTS.IDENTITY}/?${ROUTE_PARAMS.DEVICE_ID}=${encodeURIComponent(item.deviceId)}`}>
{item.deviceId}
</NavLink>
);
case 'status':
return (
<Label
key={column.key}
>
{item.status}
</Label>
);
case 'connection':
return (
<Label
key={column.key}
>
{item.connectionState}
</Label>
);
case 'authenticationType':
return (
<Label
key={column.key}
>
{item.authenticationType}
</Label>
);
case 'lastActivityTime':
return (
<Label
key={column.key}
>
{item.lastActivityTime || '--'}
</Label>
);
case 'statusUpdatedTime':
return (
<Label
key={column.key}
>
{item.statusUpdatedTime || '--'}
</Label>
);
case 'edge':
const isEdge = item.iotEdge;
return (
<Icon
key={column.key}
iconName={isEdge && CHECK}
ariaLabel={isEdge ?
context.t(ResourceKeys.deviceLists.columns.isEdgeDevice.yes) : context.t(ResourceKeys.deviceLists.columns.isEdgeDevice.no)}
/>
);
default:
return;
}
}
private readonly fetchPage = (pageNumber: number) => {
const { query } = this.props;
return this.props.listDevices({
@ -307,10 +318,6 @@ class DeviceListComponent extends React.Component<DeviceListDataProps & DeviceLi
showDeleteConfirmation: false
});
}
private readonly onRowSelection = (devices: DeviceSummary[]) => {
this.setState({ selectedDeviceIds: devices.map(device => device.deviceId) });
}
}
export default withRouter(DeviceListComponent);

Просмотреть файл

@ -1,48 +0,0 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import 'jest';
import * as React from 'react';
import { Icon } from 'office-ui-fabric-react/lib/Icon';
import { DeviceListCell, DeviceListCellProps } from './deviceListCell';
import { mountWithLocalization, testSnapshot } from '../../../shared/utils/testHelpers';
describe('components/devices/deviceListCell', () => {
const deviceListCellProps: DeviceListCellProps = {
connectionString: 'string',
device: {
authenticationType: 'sas',
cloudToDeviceMessageCount: '0',
connectionState: 'connected',
deviceId: 'testDeviceId',
iotEdge: false,
lastActivityTime: '0001-01-01T00:00:00Z',
status: 'Enabled',
statusUpdatedTime: '0001-01-01T00:00:00Z'
},
itemIndex: 1,
};
const getComponent = (overrides = {}) => {
const props = {
...deviceListCellProps,
...overrides,
};
return <DeviceListCell {...props} />;
};
it('matches snapshot while loading', () => {
testSnapshot(getComponent());
});
it('render cell with pnp icon when interface ids are retrieved', () => {
const wrapper = mountWithLocalization(getComponent());
const cell = wrapper.find(DeviceListCell);
cell.setState({isLoading: false, interfaceIds: ['interfaceId1']});
wrapper.update();
const icon = wrapper.find(Icon).first();
expect(icon.props().iconName).toEqual('pnp-svg');
});
});

Просмотреть файл

@ -1,144 +0,0 @@
import * as React from 'react';
import { Icon } from 'office-ui-fabric-react/lib/Icon';
import { registerIcons } from 'office-ui-fabric-react/lib/Styling';
import { DeviceSummary } from '../../../api/models/deviceSummary';
import { LocalizationContextConsumer, LocalizationContextInterface } from '../../../shared/contexts/localizationContext';
import { ResourceKeys } from '../../../../localization/resourceKeys';
import { fetchDigitalTwinInterfaceProperties } from '../../../api/services/devicesService';
import { DigitalTwinInterfaces } from '../../../api/models/digitalTwinModels';
import { modelDiscoveryInterfaceName } from '../../../constants/modelDefinitionConstants';
import { getReportedInterfacesFromDigitalTwin } from '../../deviceContent/selectors';
import '../../../css/_deviceListCell.scss';
// tslint:disable
registerIcons({
icons: {
'pnp-svg': (
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 64 64" enableBackground="new 0 0 64 64">
<path fill="#2F76BC" d="M58.7,9.9L58.7,9.9c-0.4,0-0.8,0-0.8,0.4L50.2,20c-2.8-6.9-9.3-11.7-17.4-11.3c-8.1,0-15,4.9-17.8,12.2
C6.9,21.6,0,27.7,0,37.4S8.5,54,17.8,54h31.6C57.5,54,64,47.1,64,39.8c0-6.1-4.1-11.3-9.7-13l9.3-12.2c0,0,0.4-0.4,0-0.8l0,0V9.9
L58.7,9.9z"/>
<path fill="#75D0EB" d="M17.8,32.9l-1.2-1.6c-0.4-0.4-0.4-1.2,0-1.6l4.1-3.6c0.4,0,0.4-0.4,0.8-0.4c0.4,0,0.4,0,0.8,0.4l10.9,11.7
l24.7-32c0.4-0.4,0.4-0.4,0.8-0.4c0.4,0,0.4,0,0.4,0l4.5,3.2C64,8.6,64,9,64,9.5c0,0.4,0,0.4-0.4,0.8L34,48.3
c-0.4,0.4-1.2,0.4-1.6,0L17.8,32.9z"/>
</svg>
)
}
});
//tslint: enable
export interface DeviceListCellProps {
itemIndex: number | string;
connectionString: string;
device: DeviceSummary;
}
export interface DeviceListCellState {
isLoading: boolean;
interfaceIds: string[];
}
export class DeviceListCell extends React.PureComponent<DeviceListCellProps, DeviceListCellState> {
private isComponentMounted: boolean;
constructor(props: DeviceListCellProps) {
super(props);
this.state = {
isLoading: true,
interfaceIds: []
};
}
public render() {
const { itemIndex, device } = this.props;
return (
<LocalizationContextConsumer>
{(context: LocalizationContextInterface) => (
!this.state.isLoading ?
<div className="device-list-cell-container" data-selection-index={itemIndex}>
{this.renderCellDeviceInfo(device, context)}
{this.state.interfaceIds.length !== 0 && this.renderCellInterfaceInfo(context)}
</div>:
<div className="device-list-cell-container" data-selection-index={itemIndex}>
{this.renderLoadingInfo(context)}
</div>
)}
</LocalizationContextConsumer>
);
}
public componentWillMount() {
this.isComponentMounted = true;
fetchDigitalTwinInterfaceProperties({
connectionString: this.props.connectionString,
digitalTwinId: this.props.device.deviceId
}).then((results: DigitalTwinInterfaces) => {
let interfaceIds = [];
if (getReportedInterfacesFromDigitalTwin(results)) {
interfaceIds = Object.keys(results.interfaces[modelDiscoveryInterfaceName].properties.modelInformation.reported.value.interfaces).map(
key => results.interfaces[modelDiscoveryInterfaceName].properties.modelInformation.reported.value.interfaces[key]
);
}
if (this.isComponentMounted) {
this.setState({
interfaceIds,
isLoading: false
});
}
}).catch(() => {
if (this.isComponentMounted) {
this.setState({
interfaceIds: [],
isLoading: false
});
}
});
}
public componentWillUnmount() {
this.isComponentMounted = false;
}
private readonly renderCellDeviceInfo = (device: DeviceSummary, context: LocalizationContextInterface) => {
return (
<div className="device-list-cell-container-content">
<span className="device-list-cell-item first">{`${context.t(ResourceKeys.deviceLists.columns.lastActivityTime)}: `}<span className="data">{device.lastActivityTime || context.t(ResourceKeys.deviceLists.noData)}</span></span>
<span className={`device-list-cell-item ${this.state.interfaceIds.length !== 0 ? '' : 'last'}`}>{`${context.t(ResourceKeys.deviceLists.columns.statusUpdatedTime)}: `}<span className="data">{device.statusUpdatedTime || context.t(ResourceKeys.deviceLists.noData)}</span></span>
{this.state.interfaceIds.length !== 0 &&
<span className="device-list-cell-item last">
<Icon
iconName="pnp-svg"
className="icon"
ariaLabel={context.t(ResourceKeys.deviceLists.columns.isPnpDevice)}
/>
{context.t(ResourceKeys.deviceLists.columns.isPnpDevice)}
</span>
}
</div>
);
}
private readonly renderCellInterfaceInfo = (context: LocalizationContextInterface) => {
const listInterfaces = this.state.interfaceIds.join("; ");
return (
<div className="device-list-cell-container-content">
{<span className="device-list-cell-item first no-border">
{`${context.t(ResourceKeys.deviceLists.columns.interfaces)}: `}
<span className="data">{listInterfaces}</span>
</span>}
</div>
);
}
private readonly renderLoadingInfo = (context: LocalizationContextInterface) => {
return (
<div className="device-list-cell-container-content">
<span>{context.t(ResourceKeys.common.loading)}</span>
</div>
);
}
}

Просмотреть файл

@ -1,409 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`components/GroupedList render GroupedList matches snapshot 1`] = `
<div
className="grouped-list"
>
<div>
<div
className="grouped-list-header"
style={
Object {
"alignItems": "center",
"columnGap": "5px",
"display": "grid",
"gridTemplateColumns": "2% 10% 10% auto",
}
}
>
<Component
key="0"
style={
Object {
"fontSize": 12,
"gridColumnStart": 2,
}
}
tooltipText="Text"
>
Name
</Component>
<Component
key="1"
style={
Object {
"fontSize": 12,
"gridColumnStart": 3,
}
}
tooltipText="Text"
>
Body
</Component>
</div>
<div
className="grouped-list-header-border"
/>
</div>
<StyledMarqueeSelectionBase
isEnabled={true}
selection={
Selection {
"_anchoredIndex": 0,
"_canSelectItem": [Function],
"_changeEventSuppressionCount": 0,
"_exemptedCount": 0,
"_exemptedIndices": Object {},
"_getKey": [Function],
"_isModal": false,
"_items": Array [
Object {
"count": 1,
"data": Object {
"Body": "World",
"Name": "Hello",
},
"isCollapsed": true,
"key": "Hello0",
"name": "Hello",
"startIndex": 0,
},
Object {
"count": 1,
"data": Object {
"Body": "World1",
"Name": "Hello1",
},
"isCollapsed": true,
"key": "Hello11",
"name": "Hello1",
"startIndex": 1,
},
],
"_keyToIndexMap": Object {
"Hello0": 0,
"Hello11": 1,
},
"_onSelectionChanged": [Function],
"_selectedItems": null,
"_unselectableCount": 0,
"_unselectableIndices": Object {
"0": false,
"1": false,
},
"count": 0,
"mode": 2,
}
}
>
<SelectionZone
isSelectedOnFocus={true}
selection={
Selection {
"_anchoredIndex": 0,
"_canSelectItem": [Function],
"_changeEventSuppressionCount": 0,
"_exemptedCount": 0,
"_exemptedIndices": Object {},
"_getKey": [Function],
"_isModal": false,
"_items": Array [
Object {
"count": 1,
"data": Object {
"Body": "World",
"Name": "Hello",
},
"isCollapsed": true,
"key": "Hello0",
"name": "Hello",
"startIndex": 0,
},
Object {
"count": 1,
"data": Object {
"Body": "World1",
"Name": "Hello1",
},
"isCollapsed": true,
"key": "Hello11",
"name": "Hello1",
"startIndex": 1,
},
],
"_keyToIndexMap": Object {
"Hello0": 0,
"Hello11": 1,
},
"_onSelectionChanged": [Function],
"_selectedItems": null,
"_unselectableCount": 0,
"_unselectableIndices": Object {
"0": false,
"1": false,
},
"count": 0,
"mode": 2,
}
}
selectionMode={2}
>
<StyledGroupedListBase
groupProps={
Object {
"onRenderHeader": [Function],
}
}
groups={
Array [
Object {
"count": 1,
"data": Object {
"Body": "World",
"Name": "Hello",
},
"isCollapsed": true,
"key": "Hello0",
"name": "Hello",
"startIndex": 0,
},
Object {
"count": 1,
"data": Object {
"Body": "World1",
"Name": "Hello1",
},
"isCollapsed": true,
"key": "Hello11",
"name": "Hello1",
"startIndex": 1,
},
]
}
items={
Array [
Object {
"Body": "World",
"Name": "Hello",
},
Object {
"Body": "World1",
"Name": "Hello1",
},
]
}
onRenderCell={[Function]}
onShouldVirtualize={[Function]}
selection={
Selection {
"_anchoredIndex": 0,
"_canSelectItem": [Function],
"_changeEventSuppressionCount": 0,
"_exemptedCount": 0,
"_exemptedIndices": Object {},
"_getKey": [Function],
"_isModal": false,
"_items": Array [
Object {
"count": 1,
"data": Object {
"Body": "World",
"Name": "Hello",
},
"isCollapsed": true,
"key": "Hello0",
"name": "Hello",
"startIndex": 0,
},
Object {
"count": 1,
"data": Object {
"Body": "World1",
"Name": "Hello1",
},
"isCollapsed": true,
"key": "Hello11",
"name": "Hello1",
"startIndex": 1,
},
],
"_keyToIndexMap": Object {
"Hello0": 0,
"Hello11": 1,
},
"_onSelectionChanged": [Function],
"_selectedItems": null,
"_unselectableCount": 0,
"_unselectableIndices": Object {
"0": false,
"1": false,
},
"count": 0,
"mode": 2,
}
}
selectionMode={2}
/>
</SelectionZone>
</StyledMarqueeSelectionBase>
</div>
`;
exports[`components/GroupedList render GroupedList matches snapshot when in a loading state 1`] = `
<div
className="grouped-list"
>
<div>
<div
className="grouped-list-header"
style={
Object {
"alignItems": "center",
"columnGap": "5px",
"display": "grid",
"gridTemplateColumns": "2% 10% 10% auto",
}
}
>
<Component
key="0"
style={
Object {
"fontSize": 12,
"gridColumnStart": 2,
}
}
tooltipText="Text"
>
Name
</Component>
<Component
key="1"
style={
Object {
"fontSize": 12,
"gridColumnStart": 3,
}
}
tooltipText="Text"
>
Body
</Component>
</div>
<div
className="grouped-list-header-border"
/>
</div>
<Component
shimmerCount={10}
/>
</div>
`;
exports[`components/GroupedList render GroupedList matches snapshot when no items 1`] = `
<div
className="grouped-list"
>
<div>
<div
className="grouped-list-header"
style={
Object {
"alignItems": "center",
"columnGap": "5px",
"display": "grid",
"gridTemplateColumns": "2% 10% 10% auto",
}
}
>
<Component
key="0"
style={
Object {
"fontSize": 12,
"gridColumnStart": 2,
}
}
tooltipText="Text"
>
Name
</Component>
<Component
key="1"
style={
Object {
"fontSize": 12,
"gridColumnStart": 3,
}
}
tooltipText="Text"
>
Body
</Component>
</div>
<div
className="grouped-list-header-border"
/>
</div>
<h3>
No Items
</h3>
<StyledAnnouncedBase
message="No Items"
/>
</div>
`;
exports[`components/GroupedList render GroupedList matches snapshot with undefined items 1`] = `
<div
className="grouped-list"
>
<div>
<div
className="grouped-list-header"
style={
Object {
"alignItems": "center",
"columnGap": "5px",
"display": "grid",
"gridTemplateColumns": "2% 10% 10% auto",
}
}
>
<Component
key="0"
style={
Object {
"fontSize": 12,
"gridColumnStart": 2,
}
}
tooltipText="Text"
>
Name
</Component>
<Component
key="1"
style={
Object {
"fontSize": 12,
"gridColumnStart": 3,
}
}
tooltipText="Text"
>
Body
</Component>
</div>
<div
className="grouped-list-header-border"
/>
</div>
<h3>
No Items
</h3>
<StyledAnnouncedBase
message="No Items"
/>
</div>
`;

Просмотреть файл

@ -1,81 +0,0 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import 'jest';
import * as React from 'react';
import { shallow } from 'enzyme';
import GroupedList, { GroupedListProps } from './groupedList';
describe('components/GroupedList', () => {
const getComponent = (overrides = {}) => {
const testItems = [
{
Body: 'World',
Name: 'Hello',
},
{
Body: 'World1',
Name: 'Hello1'
}
];
const onRenderCell = () => (<div/>);
const defaultColumnInfo = [
{
infoText: 'Text',
name: 'Name',
onRenderColumn: group => {
return (<span>{group.name}</span>);
},
widthPercentage: 10
},
{
infoText: 'Text',
name: 'Body',
onRenderColumn: group => {
return (<span>{group.name}</span>);
},
widthPercentage: 10
}
];
const props: GroupedListProps<typeof testItems[0]> = {
columnInfo: defaultColumnInfo,
isLoading: false,
items: testItems,
nameKey: 'Name',
noItemsMessage: 'No Items',
onRenderCell,
...overrides
};
return <GroupedList {...props} />;
};
context('render GroupedList', () => {
it('matches snapshot', () => {
const wrapper = shallow(getComponent());
expect(wrapper).toMatchSnapshot();
});
it('matches snapshot when no items', () => {
const wrapper = shallow(getComponent({items: []}));
expect(wrapper).toMatchSnapshot();
});
it('matches snapshot with undefined items', () => {
const wrapper = shallow(getComponent({items: undefined}));
expect(wrapper).toMatchSnapshot();
});
it('matches snapshot when in a loading state', () => {
const wrapper = shallow(getComponent({isLoading: true}));
expect(wrapper).toMatchSnapshot();
});
});
});

Просмотреть файл

@ -1,263 +0,0 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import * as React from 'react';
import { GroupedList, IGroup, IGroupDividerProps } from 'office-ui-fabric-react/lib/GroupedList';
import { IListProps } from 'office-ui-fabric-react/lib/List';
import { Checkbox } from 'office-ui-fabric-react/lib/Checkbox';
import { IconButton, BaseButton, Button } from 'office-ui-fabric-react/lib/Button';
import { ISelection, SelectionMode, Selection, SelectionZone } from 'office-ui-fabric-react/lib/Selection';
import { MarqueeSelection } from 'office-ui-fabric-react/lib/MarqueeSelection';
import { Announced } from 'office-ui-fabric-react/lib/Announced';
import LabelWithTooltip from '../labelWithTooltip';
import { GroupedList as GroupedListIconNames } from '../../../constants/iconNames';
import { CHECKBOX_WIDTH_PERCENTAGE, GRID_STYLE_CONSTANTS, CHECKBOX_WIDTH_PIXELS, LABEL_FONT_SIZE } from './groupedListStyleConstants';
import MultiLineShimmer from '../multiLineShimmer';
import '../../../css/_groupedList.scss';
const SHIMMER_COUNT = 10;
export interface GroupedListProps<T> {
isLoading: boolean;
items: T[];
nameKey: keyof T;
noItemsMessage: string;
columnInfo?: GroupedListColumn[];
onRenderCell: (nestingDepth?: number, item?: T, index?: number) => React.ReactNode;
onSelectionChanged?: (selectedItems: T[]) => void;
}
export interface GroupedListColumn {
name: string;
infoText?: string;
widthPercentage: number;
onRenderColumn: (group: IGroup, key?: string | number) => JSX.Element;
}
export interface GroupedListState {
groups: IGroup[];
selection: ISelection;
selectionMode: SelectionMode;
}
export default class GroupedListWrapper<T> extends React.Component<GroupedListProps<T>, GroupedListState> {
constructor(props: GroupedListProps<T>) {
super(props);
this.state = {
groups: [],
selection: new Selection({ onSelectionChanged: this.onSelectionChanged }),
selectionMode: SelectionMode.multiple
};
}
public render() {
const { columnInfo, items, isLoading, noItemsMessage } = this.props;
const { selection, selectionMode } = this.state;
return (
<div className="grouped-list">
{this.renderListHeader(columnInfo)}
{!!isLoading ?
<MultiLineShimmer shimmerCount={SHIMMER_COUNT}/>
: !items || items.length === 0 ? (
<>
<h3>{noItemsMessage}</h3>
<Announced
message={noItemsMessage}
/>
</>
) : (
<MarqueeSelection selection={selection} isEnabled={selection.mode === SelectionMode.multiple}>
<SelectionZone selection={selection}>
<GroupedList
items={this.props.items}
groups={this.state.groups}
groupProps={{
onRenderHeader: this.onRenderHeader
}}
onRenderCell={this.onRenderCell}
selection={selection}
selectionMode={selectionMode}
onShouldVirtualize={this.onShouldVirtualize}
/>
</SelectionZone>
</MarqueeSelection>
)
}
</div>
);
}
private onShouldVirtualize = (props: IListProps): boolean => {
return false;
}
public static getDerivedStateFromProps<T>(props: GroupedListProps<T>, state: GroupedListState): Partial<GroupedListState> | null {
if (typeof props.items !== 'undefined' && typeof props.nameKey !== 'undefined') {
const groups = GroupedListWrapper.createGroups(props);
state.selection.setItems(groups, false);
return {
groups
};
}
return null;
}
private onSelectionChanged = (): void => {
const { onSelectionChanged } = this.props;
const { groups, selection } = this.state;
if (!!onSelectionChanged) {
const items = groups.filter(group => selection.isIndexSelected(group.startIndex)).map(group => {
return group.data;
});
onSelectionChanged(items);
}
}
private static createGroups<T>(props: GroupedListProps<T>) {
return (props.items && props.items.map((item, index): IGroup => {
const itemName: string = item[props.nameKey] && item[props.nameKey].toString();
return {
count: 1,
data: item,
isCollapsed: true,
key: itemName + index,
name: itemName,
startIndex: index
};
})) || [];
}
private readonly onRenderCell = (nestingDepth?: number, item?: T, index?: number) => {
return (
<div className="grouped-list-group-cell">
{this.props.onRenderCell(nestingDepth, item, index)}
</div>
);
}
private readonly onRenderHeader = (props: IGroupDividerProps) => {
const { columnInfo } = this.props;
const { selection } = this.state;
const columns = columnInfo || [];
const toggleCollapse = (event: Event | React.MouseEvent<HTMLDivElement | HTMLAnchorElement | HTMLButtonElement | BaseButton | Button | HTMLSpanElement>): void => {
props.onToggleCollapse!(props.group!);
(event as Event).cancelBubble = true; // tslint:disable-line
if (event.stopPropagation) {
event.stopPropagation();
}
};
const renderColumns = columns.map((column, index) => {
return column.onRenderColumn(props.group, index);
});
const customStyles = {
height: GRID_STYLE_CONSTANTS.HEADER_HEIGHT
};
const styles = {...this.generateColumnStyle(columns), ...customStyles};
const onCheckboxChange = (ev?: React.FormEvent<HTMLElement | HTMLInputElement>, checked?: boolean) => {
selection.setIndexSelected(props.group.startIndex, !!checked, false);
};
const isSelected = selection && selection.isIndexSelected(props.group.startIndex);
return (
<div className={'grouped-list-group-header'} key={props.group.key} onClick={toggleCollapse} data-selection-disabled={true} data-is-focusable={true} data-selection-index={props.group.startIndex}>
<div
className={`grouped-list-group-content ${props.group.isCollapsed ? '' : 'expanded'}`}
style={styles}
>
<Checkbox
className={'grouped-list-group-checkbox'}
styles={
{
checkbox: {
borderRadius: `${CHECKBOX_WIDTH_PIXELS}px`,
height: `${CHECKBOX_WIDTH_PIXELS}px`,
width: `${CHECKBOX_WIDTH_PIXELS}px`
}
}
}
checked={isSelected}
onChange={onCheckboxChange}
/>
{renderColumns}
<IconButton
className="collapse"
iconProps={{
iconName: props.group.isCollapsed ? GroupedListIconNames.OPEN : GroupedListIconNames.CLOSE,
}}
onClick={toggleCollapse}
/>
</div>
</div>
);
}
private readonly renderListHeader = (columns: GroupedListColumn[] = []) => {
const columnOffset = 2;
const styles = this.generateColumnStyle(columns);
return (
<div>
<div
className={'grouped-list-header'}
style={styles}
>
{
columns.map((column, index) => {
return (
<LabelWithTooltip
key={index}
style={{
fontSize: LABEL_FONT_SIZE,
gridColumnStart: index + columnOffset
}}
tooltipText={column.infoText}
>
{column.name}
</LabelWithTooltip>
);
})
}
</div>
<div className="grouped-list-header-border" />
</div>
);
}
private readonly generateColumnString = (columns?: GroupedListColumn[]) => {
let columnsString = `${CHECKBOX_WIDTH_PERCENTAGE}% `;
columns.forEach(column => {
columnsString += column.widthPercentage + '% ';
});
columnsString += 'auto';
return columnsString;
}
private readonly generateColumnStyle = (columns?: GroupedListColumn[]) => {
const columnString = this.generateColumnString(columns);
return {
alignItems: GRID_STYLE_CONSTANTS.ALIGN_ITEMS,
columnGap: GRID_STYLE_CONSTANTS.COLUMN_GAP,
display: GRID_STYLE_CONSTANTS.DISPLAY,
gridTemplateColumns: columnString
};
}
}

Просмотреть файл

@ -1,14 +0,0 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
export enum GRID_STYLE_CONSTANTS {
ALIGN_ITEMS = 'center',
COLUMN_GAP = '5px',
DISPLAY = 'grid',
GRID_TEMPLATE_COLUMNS = '2% auto',
HEADER_HEIGHT = '40px'
}
export const LABEL_FONT_SIZE = 12;
export const CHECKBOX_WIDTH_PIXELS = 14;
export const CHECKBOX_WIDTH_PERCENTAGE = 2;

Просмотреть файл

@ -1,7 +0,0 @@
/***********************************************************
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License
**********************************************************/
import GroupedList from './groupedList';
export default GroupedList;