Added Web Selector to let user drill down to web (#124)
* rc0 * rc0 * more rc0 * rc0 * RC0 Done * cleanup manifest * Working on Web Selecor * qqqqqqq * 111 * Added Web selector to drill down to web * fix merge conflicts * s * renmove react-router * added saveall and undoall * Add support for ID field * saveAll * fixed saveall across sites * mark items as deleted (soft delete) * work on soft delete * work on soft delete * Enable soft deletes * Add SortInfo to columnDefinitions * Work on initial sort * pass columnDefinitions into listitemreducer for sort * trying to get sortablecolumns in reducer... * setup sort sequences * work on compareres * work on sort * basic sorting workoing * fix linting errors
This commit is contained in:
parent
dd51452dd9
commit
f137105fb4
|
@ -15,8 +15,6 @@
|
|||
"react-data-grid": "^1.0.62",
|
||||
"react-dom": "0.14.8",
|
||||
"react-redux": "^4.4.6",
|
||||
"react-router": "^2.8.1",
|
||||
"react-router-redux": "^4.0.5",
|
||||
"redux": "^3.4.0",
|
||||
"redux-localstorage": "^0.4.1",
|
||||
"redux-logger": "^2.6.0",
|
||||
|
|
|
@ -2,9 +2,8 @@ import * as React from "react";
|
|||
import * as ReactDom from "react-dom";
|
||||
import { Provider } from "react-redux";
|
||||
import configureStore from "./store/configure-store";
|
||||
//const { Router, createMemoryHistory } = require("react-router");
|
||||
import { Router, createMemoryHistory } from "react-router";
|
||||
import * as Redux from "redux";
|
||||
import AppContainer from "./containers/App";
|
||||
import { addColumns, removeAllColumns } from "./actions/columnActions";
|
||||
import { addLists, removeAllLists } from "./actions/listActions";
|
||||
import { PropertyFieldColumnDefinitions, IPropertyFieldColumnDefinitionsProps } from "./containers/PropertyFieldColumnDefinitions";
|
||||
|
@ -12,21 +11,18 @@ import { PropertyFieldListDefinitions, IPropertyFieldListDefinitionsProps } from
|
|||
import {
|
||||
BaseClientSideWebPart,
|
||||
IPropertyPaneConfiguration,
|
||||
IWebPartContext,
|
||||
PropertyPaneTextField
|
||||
} from "@microsoft/sp-webpart-base";
|
||||
debugger;
|
||||
|
||||
import { Log } from "@microsoft/sp-core-library";
|
||||
import routes from "./store/routes";
|
||||
|
||||
import * as strings from "spfxReactGridStrings";
|
||||
import { ISpfxReactGridWebPartProps } from "./ISpfxReactGridWebPartProps";
|
||||
const store: Redux.Store<any> = configureStore({});
|
||||
const history = createMemoryHistory(location);
|
||||
|
||||
const App: React.StatelessComponent<any> = () => (
|
||||
<Provider store={store}>
|
||||
<Router history={history}>
|
||||
{routes}
|
||||
</Router>
|
||||
<AppContainer />
|
||||
</Provider>
|
||||
);
|
||||
export default class SpfxReactGridWebPart extends BaseClientSideWebPart<ISpfxReactGridWebPartProps> {
|
||||
|
@ -34,7 +30,7 @@ export default class SpfxReactGridWebPart extends BaseClientSideWebPart<ISpfxRea
|
|||
private cdProps: IPropertyFieldColumnDefinitionsProps;
|
||||
private ldProps: IPropertyFieldListDefinitionsProps;
|
||||
public constructor() {
|
||||
debugger;
|
||||
|
||||
super();
|
||||
this.onPropertyChange = this.onPropertyChange.bind(this);
|
||||
this.cdProps = {
|
||||
|
|
|
@ -26,8 +26,9 @@ import * as _ from "lodash";
|
|||
import { Web, TypedHash } from "sp-pnp-js";
|
||||
import ListItem from "../model/ListItem";
|
||||
import GridRowStatus from "../Model/GridRowStatus";
|
||||
|
||||
import { Log } from "@microsoft/sp-core-library";
|
||||
import ListDefinition from "../model/ListDefinition";
|
||||
import ColumnDefinition from "../model/ColumnDefinition";
|
||||
export function clearListItems() {
|
||||
return {
|
||||
type: CLEAR_LISTITEMS,
|
||||
|
@ -48,8 +49,7 @@ export function removeListItem(dispatch: any, listItem: ListItem, listDefinition
|
|||
const listid = utils.ParseSPField(listDefinition.listLookup).id;
|
||||
const web = new Web(weburl);
|
||||
switch (listItem.__metadata__GridRowStatus) {
|
||||
case GridRowStatus.modified:
|
||||
case GridRowStatus.pristine:
|
||||
case GridRowStatus.toBeDeleted:
|
||||
web.lists.getById(listid).items.getById(listItem.ID).recycle()
|
||||
.then((response) => {
|
||||
// shouwld have an option to rfresh here in cas of calculated columns
|
||||
|
@ -67,6 +67,8 @@ export function removeListItem(dispatch: any, listItem: ListItem, listDefinition
|
|||
listItem: listItem
|
||||
}
|
||||
};
|
||||
default:
|
||||
Log.warn("ListItemContainer", "Invalid GrodrowStatus in update ListiteRender-- " + listItem.__metadata__GridRowStatus.toString());
|
||||
}
|
||||
}
|
||||
export function removeListItemSuccessAction(listItem) {
|
||||
|
@ -112,7 +114,7 @@ export function listDefinitionIsValid(listDefinition: ListDefinition): boolean {
|
|||
* Action to update a listitem in sharepoint
|
||||
*/
|
||||
export function updateListItemAction(dispatch: any, listDefinition: ListDefinition, listItem: ListItem): any {
|
||||
// listDefinition = this.getListDefinition(listItem.__metadata__ListDefinitionId);// The list Definition this item is associated with.
|
||||
|
||||
const weburl = utils.ParseSPField(listDefinition.webLookup).id;
|
||||
const listid = utils.ParseSPField(listDefinition.listLookup).id;
|
||||
const web = new Web(weburl);
|
||||
|
@ -120,6 +122,9 @@ export function updateListItemAction(dispatch: any, listDefinition: ListDefiniti
|
|||
for (const columnRef of listDefinition.columnReferences) {
|
||||
let fieldName = utils.ParseSPField(columnRef.name).id;
|
||||
switch (columnRef.fieldDefinition.TypeAsString) {
|
||||
case "Counter": // do not send ID to shareppoint as a data field
|
||||
break;
|
||||
|
||||
case "Lookup":
|
||||
if (listItem[fieldName]) {// field may not be set
|
||||
typedHash[fieldName + "Id"] = listItem[fieldName].Id;
|
||||
|
@ -280,10 +285,9 @@ export function gotListItemAction(item) {
|
|||
}
|
||||
};
|
||||
}
|
||||
export function getListItemsAction(dispatch: any, listDefinitions: Array<ListDefinition>): any {
|
||||
export function getListItemsAction(dispatch: any, listDefinitions: Array<ListDefinition>, columnDefinitions: Array<ColumnDefinition>): any {
|
||||
dispatch(clearListItems());
|
||||
|
||||
const promises: Array<Promise<any>> = new Array<Promise<any>>();
|
||||
const promises: Array<Promise<any>> = new Array<Promise<any>>();
|
||||
for (const listDefinition of listDefinitions) {
|
||||
if (!listDefinitionIsValid(listDefinition)) {
|
||||
break;
|
||||
|
@ -326,7 +330,7 @@ export function getListItemsAction(dispatch: any, listDefinitions: Array<ListDef
|
|||
return item;
|
||||
});
|
||||
console.log(data);
|
||||
const gotListItems = gotListItemsAction(data);
|
||||
const gotListItems = gotListItemsAction(data,listDefinitions,columnDefinitions);
|
||||
dispatch(gotListItems); // need to ewname this one to be digfferent from the omported ome
|
||||
})
|
||||
.catch((error) => {
|
||||
|
@ -343,6 +347,7 @@ export function getListItemsAction(dispatch: any, listDefinitions: Array<ListDef
|
|||
promise: Promise.all(promises)
|
||||
}
|
||||
};
|
||||
|
||||
return action;
|
||||
}
|
||||
export function getListItemsErrorAction(error) {
|
||||
|
@ -354,11 +359,13 @@ export function getListItemsErrorAction(error) {
|
|||
};
|
||||
|
||||
}
|
||||
export function gotListItemsAction(items) {
|
||||
export function gotListItemsAction(items: Array<ListItem>, listDefinitions: Array<ListDefinition>, columnDefinitions: Array<ColumnDefinition>) {
|
||||
return {
|
||||
type: GOT_LISTITEMS,
|
||||
payload: {
|
||||
items: items
|
||||
items: items,
|
||||
listDefinitions: listDefinitions,
|
||||
columnDefinitions: columnDefinitions
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
import * as React from "react";
|
||||
import { Web } from "../model/Site";
|
||||
export interface KeyValue {
|
||||
value: any;
|
||||
displayName: string;
|
||||
}
|
||||
import { Dropdown, IDropdownOption } from "office-ui-fabric-react";
|
||||
export interface IWebEditorProps extends React.Props<any> {
|
||||
selectedValue?: string;
|
||||
onChange(event): void;
|
||||
webs: Array<Web>;
|
||||
}
|
||||
export default class WebEditor extends React.Component<IWebEditorProps, void> {
|
||||
constructor() {
|
||||
super();
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
}
|
||||
private handleChange(selectedItem:IDropdownOption) {
|
||||
this.props.onChange(selectedItem.key);
|
||||
}
|
||||
public render() {
|
||||
const { selectedValue, webs} = this.props;
|
||||
|
||||
let options: Array<IDropdownOption> = webs.map((web) => {
|
||||
return ({
|
||||
key: web.url + "#;" + web.title,
|
||||
text: web.title
|
||||
});
|
||||
});
|
||||
options.unshift({ key: null, text: "..Select One" });
|
||||
return (
|
||||
<Dropdown label="" selectedKey={selectedValue} options={options} onChanged={this.handleChange} >
|
||||
</Dropdown >
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
import { Web } from "../model/Site";
|
||||
import * as _ from "underscore";
|
||||
import * as React from 'react';
|
||||
import { Label } from 'office-ui-fabric-react/lib/Label';
|
||||
import { Button, ButtonType } from 'office-ui-fabric-react/lib/Button';
|
||||
import { Dropdown, IDropdownOption } from 'office-ui-fabric-react/lib/Dropdown';
|
||||
import { Panel, PanelType } from 'office-ui-fabric-react/lib/Panel';
|
||||
import * as utils from "../utils/utils";
|
||||
|
||||
import { Web as SPWeb } from "sp-pnp-js";
|
||||
|
||||
import { PageContext } from "@microsoft/sp-page-context";
|
||||
|
||||
export interface IWebSelectorProps {
|
||||
onChange(newValue: any): void;
|
||||
PageContext: PageContext;
|
||||
selectedWeb: string;
|
||||
siteUrl: string;
|
||||
headerText: string;
|
||||
}
|
||||
export interface IWebSelectorState {
|
||||
openPanel: boolean;
|
||||
selectedWeb: string;
|
||||
options: Array<IDropdownOption>;
|
||||
}
|
||||
export default class WebSelector extends React.Component<IWebSelectorProps, IWebSelectorState> {
|
||||
constructor(props: IWebSelectorProps) {
|
||||
super(props);
|
||||
this.onOpenPanel = this.onOpenPanel.bind(this);
|
||||
this.onClosePanel = this.onClosePanel.bind(this);
|
||||
this.getWebs = this.getWebs.bind(this);
|
||||
this.SelectedWebChanged = this.SelectedWebChanged.bind(this);
|
||||
this.state = {
|
||||
openPanel: false,
|
||||
selectedWeb: this.props.selectedWeb,
|
||||
options: []
|
||||
};
|
||||
}
|
||||
private getWebs(webUrl: string): any {
|
||||
const spWeb: SPWeb = new SPWeb(webUrl);
|
||||
const promise = spWeb.webs.orderBy("Title").get()
|
||||
.then((response) => {
|
||||
const webs = _.map(response, (item: any) => {
|
||||
const web: Web = new Web(item.Id, item.Title, item.Url);
|
||||
return web;
|
||||
});
|
||||
console.log(webs);
|
||||
return webs;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
|
||||
private onOpenPanel(element?: any): void {
|
||||
this.state.openPanel = true;
|
||||
this.setState(this.state);
|
||||
this.getWebs(this.props.siteUrl).then(webs => {
|
||||
let options: Array<IDropdownOption> = webs.map((web) => {
|
||||
return ({
|
||||
key: web.url + "#;" + web.title,
|
||||
text: web.title
|
||||
});
|
||||
});
|
||||
options.unshift({ key: "", text: "Choose a web to select it" });
|
||||
this.state.options = options;
|
||||
this.setState(this.state);
|
||||
});
|
||||
}
|
||||
private SelectedWebChanged(option: IDropdownOption, index?: number): void {
|
||||
const key: string = option.key as string;
|
||||
if (key === "") { return; }
|
||||
const webUrl = utils.ParseSPField(key).id;
|
||||
this.state.selectedWeb = key;
|
||||
this.props.onChange(key);
|
||||
this.getWebs(webUrl).then(webs => {
|
||||
let options: Array<IDropdownOption> = webs.map((web) => {
|
||||
return ({
|
||||
key: web.url + "#;" + web.title,
|
||||
text: web.title
|
||||
});
|
||||
});
|
||||
options.unshift({ key: "", text: "Select one...", selected: true });
|
||||
this.state.options = options;
|
||||
this.setState(this.state);
|
||||
});
|
||||
}
|
||||
private onClosePanel(element?: any): void {
|
||||
this.state.openPanel = false;
|
||||
this.setState(this.state);
|
||||
}
|
||||
public render(): JSX.Element {
|
||||
|
||||
return (
|
||||
<div style={{ marginBottom: '8px' }}>
|
||||
{this.state.openPanel === true ?
|
||||
<Panel
|
||||
isOpen={this.state.openPanel} hasCloseButton={true} onDismiss={this.onClosePanel}
|
||||
isLightDismiss={true} type={PanelType.smallFixedFar}
|
||||
headerText={this.props.headerText}>
|
||||
<div>
|
||||
<span> <Label>Site Url</Label> {this.props.siteUrl}</span>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Currently selected Web Url</Label> {utils.ParseSPField(this.state.selectedWeb).id}
|
||||
</div>
|
||||
<div>
|
||||
<Label>Currently selected Web Title</Label> {utils.ParseSPField(this.state.selectedWeb).value}
|
||||
</div>
|
||||
|
||||
<Dropdown label="Choose a different Web"
|
||||
options={this.state.options}
|
||||
onChanged={this.SelectedWebChanged} defaultSelectedKey="">
|
||||
</Dropdown>
|
||||
</Panel>
|
||||
: <div>
|
||||
{utils.ParseSPField(this.state.selectedWeb).value}
|
||||
<Button buttonType={ButtonType.icon} icon="Search" onClick={this.onOpenPanel}></Button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import * as React from 'react';
|
||||
/*import * as React from 'react';
|
||||
|
||||
export interface IContentProps extends React.Props<any> {
|
||||
isVisible: boolean;
|
||||
|
@ -11,4 +11,4 @@ export default function Content({ children = null, isVisible }: IContentProps) {
|
|||
{ isVisible ? children : null }
|
||||
</main>
|
||||
);
|
||||
}
|
||||
}*/
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import * as React from "react";
|
||||
import { SharePointLookupCellFormatter } from "../components/SharePointFormatters";
|
||||
//const connect = require("react-redux").connect;
|
||||
import {connect} from "react-redux";
|
||||
import { connect } from "react-redux";
|
||||
import * as _ from "lodash";
|
||||
import { addColumn, removeColumn, removeAllColumns, moveCulumnUp, moveCulumnDown } from "../actions/columnActions";
|
||||
import { addColumn, removeColumn, removeAllColumns, moveCulumnUp, moveCulumnDown } from "../actions/columnActions";
|
||||
import ColumnDefinition from "../model/ColumnDefinition";
|
||||
import { Button, ButtonType, TextField, CommandBar, Dropdown, IDropdownOption, Toggle } from "office-ui-fabric-react";
|
||||
import { SortDirection } from "../model/ColumnDefinition";
|
||||
import { Button, ButtonType, TextField, CommandBar, Dropdown, IDropdownOption, Toggle, Slider } from "office-ui-fabric-react";
|
||||
import Container from "../components/container";
|
||||
import { Guid, Log } from "@microsoft/sp-core-library";
|
||||
/** NOTE:
|
||||
|
@ -29,6 +30,7 @@ const fieldTypes: Array<IDropdownOption> = [
|
|||
// { key: "Counter", text: "Counter" },
|
||||
{ key: "Choice", text: "Choice" },
|
||||
{ key: "Lookup", text: "Lookup" },
|
||||
{ key: "Counter", text: "Coumter (Item ID)" },
|
||||
// { key: "Boolean", value: "Boolean" },
|
||||
{ key: "Number", text: "Number" },
|
||||
// { key: "Currency", value: "Currency" },
|
||||
|
@ -47,6 +49,13 @@ const fieldTypes: Array<IDropdownOption> = [
|
|||
// { name: "WorkflowStatus", value: "WorkflowStatus" },
|
||||
// { name: "WorkflowEventType", value: "WorkflowEventType" },
|
||||
|
||||
];
|
||||
const sortDirectionOptions: Array<IDropdownOption> = [
|
||||
|
||||
{ key: SortDirection.None, text: SortDirection[SortDirection.None] },
|
||||
{ key: SortDirection.Ascending, text: SortDirection[SortDirection.Ascending] },
|
||||
{ key: SortDirection.Descending, text: SortDirection[SortDirection.Descending] },
|
||||
|
||||
];
|
||||
export interface IColumnsPageProps extends React.Props<any> {
|
||||
columns: Array<ColumnDefinition>;
|
||||
|
@ -59,7 +68,7 @@ export interface IColumnsPageProps extends React.Props<any> {
|
|||
save: () => void;
|
||||
}
|
||||
interface IContextMenu extends React.Props<any> {
|
||||
// onRowDelete: AdazzleReactDataGrid.ColumnEventCallback;
|
||||
// onRowDelete: AdazzleReactDataGrid.ColumnEventCallback;
|
||||
}
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
|
@ -70,7 +79,7 @@ function mapDispatchToProps(dispatch) {
|
|||
return {
|
||||
addColumn: (): void => {
|
||||
const id = Guid.newGuid();
|
||||
const col: ColumnDefinition = new ColumnDefinition(id.toString(), "", 80, true);
|
||||
const col: ColumnDefinition = new ColumnDefinition(id.toString(), "", 80, true, );
|
||||
dispatch(addColumn(col));
|
||||
},
|
||||
|
||||
|
@ -151,6 +160,24 @@ export class ColumnDefinitionContainerNative extends React.Component<IColumnsPag
|
|||
editor: "BooleanEditor",
|
||||
formatter: "BooleanFormatter",
|
||||
width: 99
|
||||
},
|
||||
{
|
||||
id: "sortSequence",
|
||||
name: "sortSequence",
|
||||
editable: true,
|
||||
width: 99,
|
||||
editor: "SortSequenceEditor",
|
||||
|
||||
|
||||
},
|
||||
{
|
||||
id: "sortDirection",
|
||||
name: "sortDirection",
|
||||
editable: true,
|
||||
width: 99,
|
||||
editor: "SortDirectionEditor",
|
||||
formatter: "SortDirectionFormatter",
|
||||
|
||||
}];
|
||||
|
||||
private handleCellUpdatedEvent(event) { //native react uses a Synthetic event
|
||||
|
@ -158,8 +185,8 @@ export class ColumnDefinitionContainerNative extends React.Component<IColumnsPag
|
|||
}
|
||||
private handleCellUpdated(value) { // Office UI Fabric does not use events. It just calls this method with the new value
|
||||
let {entityid, columnid} = this.state.editing;
|
||||
const entity: ColumnDefinition =_.find( this.props.columns,(temp) => temp.guid === entityid);
|
||||
const column = _.find(this.gridColulumns,(temp )=> temp.id === columnid);
|
||||
const entity: ColumnDefinition = _.find(this.props.columns, (temp) => temp.guid === entityid);
|
||||
const column = _.find(this.gridColulumns, (temp) => temp.id === columnid);
|
||||
entity[column.name] = value;
|
||||
// this.props.saveColumn(entity);
|
||||
|
||||
|
@ -171,7 +198,7 @@ export class ColumnDefinitionContainerNative extends React.Component<IColumnsPag
|
|||
const target = this.getParent(event.target, "TD");
|
||||
const attributes: NamedNodeMap = target.attributes;
|
||||
const entityId = attributes.getNamedItem("data-entityid").value;
|
||||
const column: ColumnDefinition =_.find( this.props.columns,cd => cd.guid === entityId);
|
||||
const column: ColumnDefinition = _.find(this.props.columns, cd => cd.guid === entityId);
|
||||
this.props.moveColumnUp(column);
|
||||
return;
|
||||
}
|
||||
|
@ -181,7 +208,7 @@ export class ColumnDefinitionContainerNative extends React.Component<IColumnsPag
|
|||
const target = this.getParent(event.target, "TD");
|
||||
const attributes: NamedNodeMap = target.attributes;
|
||||
const entityId = attributes.getNamedItem("data-entityid").value;
|
||||
const column: ColumnDefinition = _.find(this.props.columns,cd => cd.guid === entityId);
|
||||
const column: ColumnDefinition = _.find(this.props.columns, cd => cd.guid === entityId);
|
||||
this.props.moveColumnDown(column);
|
||||
return;
|
||||
}
|
||||
|
@ -191,7 +218,7 @@ export class ColumnDefinitionContainerNative extends React.Component<IColumnsPag
|
|||
const target = this.getParent(event.target, "TD");
|
||||
const attributes: NamedNodeMap = target.attributes;
|
||||
const entityId = attributes.getNamedItem("data-entityid").value;
|
||||
const column: ColumnDefinition = _.find(this.props.columns,cd => cd.guid === entityId);
|
||||
const column: ColumnDefinition = _.find(this.props.columns, cd => cd.guid === entityId);
|
||||
this.props.removeColumn(column);
|
||||
return;
|
||||
}
|
||||
|
@ -227,12 +254,30 @@ export class ColumnDefinitionContainerNative extends React.Component<IColumnsPag
|
|||
<Dropdown label="" selectedKey={entity[gridColumn.name]} options={fieldTypes} onChanged={(selection: IDropdownOption) => cellUpdated(selection.key)} >
|
||||
</Dropdown >
|
||||
);
|
||||
case "SortSequenceEditor":
|
||||
return (
|
||||
<Slider
|
||||
onChange={selection => cellUpdated(selection)}
|
||||
min={1}
|
||||
max={10}
|
||||
value={entity[gridColumn.name]}
|
||||
>
|
||||
</Slider >
|
||||
);
|
||||
case "SortDirectionEditor":
|
||||
return (
|
||||
<Dropdown label="" selectedKey={entity[gridColumn.name]}
|
||||
options={sortDirectionOptions}
|
||||
onChanged={(selection: IDropdownOption) => cellUpdated(selection.key)}
|
||||
>
|
||||
</Dropdown >
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<TextField autoFocus width={gridColumn.width}
|
||||
value={entity[gridColumn.name]}
|
||||
onChanged={cellUpdated} // this does not use eventing. It just calls the method. onChanged NOT onChange
|
||||
/>);
|
||||
/>);
|
||||
}
|
||||
}
|
||||
public CellContents(props: { entity: ColumnDefinition, gridColumn: GridColumn }): JSX.Element {
|
||||
|
@ -253,6 +298,13 @@ export class ColumnDefinitionContainerNative extends React.Component<IColumnsPag
|
|||
// );
|
||||
let result = (entity[gridColumn.name]) ? (<div>Yes</div>) : (<div>No</div>);
|
||||
return result;
|
||||
case "SortDirectionFormatter":
|
||||
return (
|
||||
<a href="#" onFocus={this.toggleEditing} style={{ textDecoration: "none" }}>
|
||||
{SortDirection[entity[gridColumn.name]]}
|
||||
</a>
|
||||
);
|
||||
|
||||
case "SharePointLookupCellFormatter":
|
||||
return (<SharePointLookupCellFormatter value={entity[gridColumn.name]} onFocus={this.toggleEditing} />);
|
||||
default:
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import * as React from "react";
|
||||
import * as utils from "../utils/utils";
|
||||
//const connect = require("react-redux").connect;
|
||||
import * as strings from "spfxReactGridStrings";
|
||||
import {connect} from "react-redux";
|
||||
import * as _ from "lodash";
|
||||
import { SharePointLookupCellFormatter } from "../components/SharePointFormatters";
|
||||
import WebEditor from "../components/WebEditor";
|
||||
import WebSelector from "../components/WebSelector";
|
||||
import ListEditor from "../components/ListEditor";
|
||||
import { addList, removeList, saveList, removeAllLists } from "../actions/listActions";
|
||||
import { getWebsAction, getListsForWebAction, getFieldsForListAction } from "../actions/SiteActions";
|
||||
|
@ -326,9 +326,15 @@ export class ListDefinitionContainerNative extends React.Component<IListViewPage
|
|||
switch (column.editor) {
|
||||
|
||||
case "WebEditor":
|
||||
|
||||
let webs = this.getWebsForSite(entity);
|
||||
return (<WebEditor webs={webs} selectedValue={columnValue} onChange={cellUpdated} />);
|
||||
return (
|
||||
<WebSelector
|
||||
selectedWeb={columnValue}
|
||||
onChange={cellUpdated}
|
||||
PageContext={this.props.pageContext}
|
||||
siteUrl={entity.siteUrl}
|
||||
headerText={strings.WebSelectorHeaderText}
|
||||
/>
|
||||
);
|
||||
case "ListEditor":
|
||||
let lists = this.getListsForWeb(entity);// the Id portion of the WebLookup is the URL
|
||||
return (<ListEditor selectedValue={columnValue} onChange={cellUpdated} lists={lists} />);
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import * as React from "react";
|
||||
|
||||
//const connect = require("react-redux").connect;
|
||||
import {connect} from "react-redux";
|
||||
import { connect } from "react-redux";
|
||||
import * as _ from "lodash";
|
||||
import {
|
||||
addListItem, removeListItem, getListItemsAction, saveListItemAction,
|
||||
|
@ -40,7 +40,7 @@ interface IListViewPageProps extends React.Props<any> {
|
|||
/** Redux Action to add a new remove a list item */
|
||||
removeListItem: (l: ListItem, ListDef: ListDefinition) => void;
|
||||
/** Redux Action to get listitems from a specific list */
|
||||
getListItems: (listDefinitions: Array<ListDefinition>) => void;
|
||||
getListItems: (listDefinitions: Array<ListDefinition>, columnDefinitions: Array<ColumnDefinition>) => void;
|
||||
/** Redux Action to update a listitem in sharepoint */
|
||||
updateListItem: (ListItem: ListItem, ListDef: ListDefinition) => Promise<any>;
|
||||
/** Redux Action to get the lookup options for a specific field */
|
||||
|
@ -53,7 +53,6 @@ interface IListViewPageProps extends React.Props<any> {
|
|||
saveListItem: (ListItem) => void;
|
||||
}
|
||||
function mapStateToProps(state) {
|
||||
|
||||
return {
|
||||
listItems: state.items,
|
||||
columns: state.columns,
|
||||
|
@ -93,8 +92,8 @@ function mapDispatchToProps(dispatch) {
|
|||
dispatch(undoListItemChangesAction(listItem));
|
||||
},
|
||||
|
||||
getListItems: (listDefinitions: Array<ListDefinition>): void => {
|
||||
dispatch(getListItemsAction(dispatch, listDefinitions));
|
||||
getListItems: (listDefinitions: Array<ListDefinition>, columnDefinitions: Array<ColumnDefinition>): void => {
|
||||
dispatch(getListItemsAction(dispatch, listDefinitions, columnDefinitions));// Column Defs needed to sort
|
||||
},
|
||||
getLookupOptionAction: (lookupSite, lookupWebId, lookupListId, lookupField): void => {
|
||||
dispatch(getLookupOptionAction(dispatch, lookupSite, lookupWebId, lookupListId, lookupField));
|
||||
|
@ -131,16 +130,36 @@ class ListItemContainer extends React.Component<IListViewPageProps, IGridState>
|
|||
this.TableRows = this.TableRows.bind(this);
|
||||
this.toggleEditing = this.toggleEditing.bind(this);
|
||||
this.addListItem = this.addListItem.bind(this);
|
||||
this.removeListItem = this.removeListItem.bind(this);
|
||||
// this.removeListItem = this.removeListItem.bind(this);
|
||||
this.handleCellUpdated = this.handleCellUpdated.bind(this);
|
||||
this.handleCellUpdatedEvent = this.handleCellUpdatedEvent.bind(this);
|
||||
this.undoItemChanges = this.undoItemChanges.bind(this);
|
||||
this.HandleUndoItemChangesEvent = this.HandleUndoItemChangesEvent.bind(this);
|
||||
this.handleUpdateListItemEvent = this.handleUpdateListItemEvent.bind(this);
|
||||
this.updateListItem = this.updateListItem.bind(this);
|
||||
this.getLookupOptions = this.getLookupOptions.bind(this);
|
||||
this.saveAll = this.saveAll.bind(this);
|
||||
this.undoAll = this.undoAll.bind(this);
|
||||
this.markListItemAsDeleted = this.markListItemAsDeleted.bind(this);
|
||||
|
||||
}
|
||||
private addListItem(): void {
|
||||
private saveAll(): void {
|
||||
const unsavedItems = _.filter(this.props.listItems, item => {
|
||||
return item.__metadata__OriginalValues;
|
||||
});
|
||||
for (const entity of unsavedItems) {
|
||||
this.updateListItem(entity);
|
||||
}
|
||||
}
|
||||
private undoAll(): void {
|
||||
const unsavedItems = _.filter(this.props.listItems, item => {
|
||||
return item.__metadata__OriginalValues;
|
||||
});
|
||||
for (const unsavedItem of unsavedItems) {
|
||||
this.props.undoItemChanges(unsavedItem);
|
||||
}
|
||||
}
|
||||
|
||||
private addListItem(): void {
|
||||
let listItem = new ListItem();
|
||||
for (const column of this.props.columns) {
|
||||
listItem[column.name] = null;
|
||||
|
@ -153,29 +172,28 @@ class ListItemContainer extends React.Component<IListViewPageProps, IGridState>
|
|||
|
||||
this.props.addListItem(listItem);
|
||||
}
|
||||
private removeListItem(event): void {
|
||||
// private removeListItem(event): void {
|
||||
|
||||
const parentTD = this.getParent(event.target, "TD");
|
||||
const attributes: NamedNodeMap = parentTD.attributes;
|
||||
const entityid = attributes.getNamedItem("data-entityid").value; // theid of the SPListItem
|
||||
const listItem: ListItem = _.find( this.props.listItems,(temp) => temp.GUID === entityid); // the listItemItself
|
||||
const listDef = this.getListDefinition(listItem.__metadata__ListDefinitionId);// The list Definition this item is associated with.
|
||||
this.props.removeListItem(listItem, listDef);
|
||||
}
|
||||
// const parentTD = this.getParent(event.target, "TD");
|
||||
// const attributes: NamedNodeMap = parentTD.attributes;
|
||||
// const entityid = attributes.getNamedItem("data-entityid").value; // theid of the SPListItem
|
||||
// const listItem: ListItem = _.find(this.props.listItems, (temp) => temp.GUID === entityid); // the listItemItself
|
||||
// const listDef = this.getListDefinition(listItem.__metadata__ListDefinitionId);// The list Definition this item is associated with.
|
||||
// this.props.removeListItem(listItem, listDef);
|
||||
// }
|
||||
/**
|
||||
* When the component Mounts, call an action to get the listitems for all the listdefinitions
|
||||
*/
|
||||
public componentWillMount() {
|
||||
|
||||
this.props.getListItems(this.props.listDefinitions);
|
||||
this.props.getListItems(this.props.listDefinitions, this.props.columns);
|
||||
}
|
||||
public componentWillReceiveProps(newProps: IListViewPageProps) {
|
||||
|
||||
if (newProps.listDefinitions === this.props.listDefinitions && newProps.columns === this.props.columns) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.getListItems(this.props.listDefinitions);
|
||||
this.props.getListItems(this.props.listDefinitions, this.props.columns);
|
||||
}
|
||||
/**
|
||||
* Method to get the parent TD of any cell,
|
||||
|
@ -208,10 +226,10 @@ class ListItemContainer extends React.Component<IListViewPageProps, IGridState>
|
|||
/**
|
||||
* Need to fire events here to get data needed for the rerender
|
||||
*/
|
||||
const listitem =_.find( this.props.listItems,li => li.GUID === entityid);
|
||||
const listitem = _.find(this.props.listItems, li => li.GUID === entityid);
|
||||
const listDef = this.getListDefinition(listitem.__metadata__ListDefinitionId);
|
||||
if (listDef) {// if user just added an item we may not hava a lisdef yest
|
||||
const colref = _.find(listDef.columnReferences,cr => cr.columnDefinitionId === columnid);
|
||||
const colref = _.find(listDef.columnReferences, cr => cr.columnDefinitionId === columnid);
|
||||
if (colref) {// Listname does not have a columnReference
|
||||
|
||||
switch (colref.fieldDefinition.TypeAsString) {
|
||||
|
@ -244,36 +262,61 @@ class ListItemContainer extends React.Component<IListViewPageProps, IGridState>
|
|||
/**
|
||||
* This event gets fired to revert any changes made to the ListItem.
|
||||
*/
|
||||
public undoItemChanges(event): void {
|
||||
public HandleUndoItemChangesEvent(event): void {
|
||||
const parentTD = this.getParent(event.target, "TD"); // the listitemId and the column ID are always stored as attributes of the parent TD.
|
||||
const attributes: NamedNodeMap = parentTD.attributes;
|
||||
const entityitem = attributes.getNamedItem("data-entityid");
|
||||
const entityid = entityitem.value;
|
||||
const entity: ListItem = _.find(this.props.listItems,(temp) => temp.GUID === entityid);
|
||||
const entity: ListItem = _.find(this.props.listItems, (temp) => temp.GUID === entityid);
|
||||
this.props.undoItemChanges(entity);
|
||||
}
|
||||
/**
|
||||
* This event gets fired, to save the item back to SharePoint.
|
||||
*/
|
||||
public updateListItem(event): void {
|
||||
public updateListItem(entity: ListItem): void {
|
||||
const listDef: ListDefinition = this.getListDefinition(entity.__metadata__ListDefinitionId);
|
||||
if (entity.__metadata__ListDefinitionId === entity.__metadata__OriginalValues.__metadata__ListDefinitionId) {// List not changed
|
||||
switch (entity.__metadata__GridRowStatus) {
|
||||
case GridRowStatus.toBeDeleted:
|
||||
this.props.removeListItem(entity, listDef);
|
||||
break;
|
||||
case GridRowStatus.new:
|
||||
case GridRowStatus.modified:
|
||||
this.props.updateListItem(entity, listDef);
|
||||
break;
|
||||
default:
|
||||
Log.warn("ListItemContainer", "Invalid GrodrowStatus in update ListiteRender-- " + entity.__metadata__GridRowStatus.toString());
|
||||
}
|
||||
|
||||
}
|
||||
else {// list changed
|
||||
const oldListDef: ListDefinition = this.getListDefinition(entity.__metadata__OriginalValues.__metadata__ListDefinitionId);
|
||||
switch (entity.__metadata__GridRowStatus) {
|
||||
case GridRowStatus.toBeDeleted: // delete from orignal list
|
||||
this.props.removeListItem(entity, oldListDef);
|
||||
break;
|
||||
case GridRowStatus.modified:
|
||||
entity.__metadata__GridRowStatus = GridRowStatus.new;
|
||||
/* falls through */
|
||||
case GridRowStatus.new:// add to new list , delete from orignal list
|
||||
this.props.updateListItem(entity, listDef).then(response => {
|
||||
this.props.removeListItem(entity.__metadata__OriginalValues, oldListDef);
|
||||
});
|
||||
break;
|
||||
default:
|
||||
Log.warn("ListItemContainer", "Invalid GrodrowStatus in update ListiteRender-- " + entity.__metadata__GridRowStatus.toString());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
public handleUpdateListItemEvent(event): void {
|
||||
|
||||
const parentTD = this.getParent(event.target, "TD");
|
||||
const attributes: NamedNodeMap = parentTD.attributes;
|
||||
const entityid = attributes.getNamedItem("data-entityid").value; // theid of the SPListItem
|
||||
const entity: ListItem = _.find(this.props.listItems,(temp) => temp.GUID === entityid);
|
||||
const listDef: ListDefinition = this.getListDefinition(entity.__metadata__ListDefinitionId);
|
||||
if (entity.__metadata__ListDefinitionId === entity.__metadata__OriginalValues.__metadata__ListDefinitionId
|
||||
|| entity.__metadata__GridRowStatus === GridRowStatus.new) {// List not changed
|
||||
|
||||
this.props.updateListItem(entity, listDef);
|
||||
}
|
||||
else {// list changed, add to new, delete from old (will need to do some fiorld mapping in here
|
||||
entity.__metadata__GridRowStatus = GridRowStatus.new;
|
||||
this.props.updateListItem(entity, listDef).then(response => {
|
||||
const oldListDef: ListDefinition = this.getListDefinition(entity.__metadata__OriginalValues.__metadata__ListDefinitionId);
|
||||
this.props.removeListItem(entity.__metadata__OriginalValues, oldListDef);
|
||||
});
|
||||
}
|
||||
const entity: ListItem = _.find(this.props.listItems, (temp) => temp.GUID === entityid);
|
||||
this.updateListItem(entity);
|
||||
|
||||
}
|
||||
/**
|
||||
|
@ -294,7 +337,7 @@ class ListItemContainer extends React.Component<IListViewPageProps, IGridState>
|
|||
const oldListDef = this.getListDefinition(listItem.__metadata__OriginalValues.__metadata__ListDefinitionId);
|
||||
for (const newColRef of newListDef.columnReferences) {
|
||||
// find the old columnReference
|
||||
const oldColRef = _.find(oldListDef.columnReferences,cr => cr.columnDefinitionId === newColRef.columnDefinitionId);
|
||||
const oldColRef = _.find(oldListDef.columnReferences, cr => cr.columnDefinitionId === newColRef.columnDefinitionId);
|
||||
const newFieldName = utils.ParseSPField(newColRef.name).id;
|
||||
const oldFieldName = utils.ParseSPField(oldColRef.name).id;
|
||||
switch (newColRef.fieldDefinition.TypeAsString) {
|
||||
|
@ -302,8 +345,8 @@ class ListItemContainer extends React.Component<IListViewPageProps, IGridState>
|
|||
// should male a local copy befor i start messing with these.// fieldd names may overlap on old and new
|
||||
// const name = listItem.__metadata__OriginalValues[oldFieldName].Name;// the user login name
|
||||
const name = listItem[oldFieldName].Name;// the user login name
|
||||
const siteUsersOnNewSite = _.find(this.props.siteUsers,su => su.siteUrl === newListDef.siteUrl);
|
||||
const newUser =_.find( siteUsersOnNewSite.siteUser,user => user.loginName === name);
|
||||
const siteUsersOnNewSite = _.find(this.props.siteUsers, su => su.siteUrl === newListDef.siteUrl);
|
||||
const newUser = _.find(siteUsersOnNewSite.siteUser, user => user.loginName === name);
|
||||
if (newUser) {
|
||||
listItem[newFieldName].Id = newUser.id;
|
||||
listItem[newFieldName].Name = newUser.loginName;
|
||||
|
@ -321,7 +364,26 @@ class ListItemContainer extends React.Component<IListViewPageProps, IGridState>
|
|||
}
|
||||
}
|
||||
/**
|
||||
* This method gets called when react cells in the gid get updated.
|
||||
* This method gets called when user clicks delete listitem.
|
||||
* Office UI Fabric does not use events. It just calls this method with the new value.
|
||||
* It reformats the data to fit the format we recievbed from SP in the first place ,
|
||||
* and dispatches an action to save the data in the store.
|
||||
*
|
||||
* Also, it saves the original version of the record, so we can undo later.
|
||||
*/
|
||||
private markListItemAsDeleted(event) {
|
||||
const parentTD = this.getParent(event.target, "TD");
|
||||
const attributes: NamedNodeMap = parentTD.attributes;
|
||||
const entityid = attributes.getNamedItem("data-entityid").value; // theid of the SPListItem
|
||||
const listItem: ListItem = _.find(this.props.listItems, (temp) => temp.GUID === entityid); // the listItemItself
|
||||
if (!listItem.__metadata__OriginalValues) { //SAVE orgininal values so we can undo;
|
||||
listItem.__metadata__OriginalValues = _.cloneDeep(listItem); // need deep if we have lookup values
|
||||
}
|
||||
listItem.__metadata__GridRowStatus = GridRowStatus.toBeDeleted;
|
||||
this.props.saveListItem(listItem);
|
||||
}
|
||||
/**
|
||||
* This method gets called when cells in the grid get updated.
|
||||
* Office UI Fabric does not use events. It just calls this method with the new value.
|
||||
* It reformats the data to fit the format we recievbed from SP in the first place ,
|
||||
* and dispatches an action to save the data in the store.
|
||||
|
@ -331,9 +393,9 @@ class ListItemContainer extends React.Component<IListViewPageProps, IGridState>
|
|||
private handleCellUpdated(value) {
|
||||
|
||||
const {entityid, columnid} = this.state.editing;
|
||||
const entity: ListItem = _.find(this.props.listItems,(temp) => temp.GUID === entityid);
|
||||
const entity: ListItem = _.find(this.props.listItems, (temp) => temp.GUID === entityid);
|
||||
const listDef = this.getListDefinition(entity.__metadata__ListDefinitionId);
|
||||
const titleColumn =_.find( this.props.columns,c => { return c.type === "__LISTDEFINITIONTITLE__"; });
|
||||
const titleColumn = _.find(this.props.columns, c => { return c.type === "__LISTDEFINITIONTITLE__"; });
|
||||
if (titleColumn) {
|
||||
if (columnid === titleColumn.guid) { // user just changed the listDef,
|
||||
if (entity.__metadata__GridRowStatus === GridRowStatus.pristine) {
|
||||
|
@ -354,7 +416,7 @@ class ListItemContainer extends React.Component<IListViewPageProps, IGridState>
|
|||
return;
|
||||
}
|
||||
}
|
||||
const columnReference = _.find(listDef.columnReferences,cr => cr.columnDefinitionId === columnid);
|
||||
const columnReference = _.find(listDef.columnReferences, cr => cr.columnDefinitionId === columnid);
|
||||
const internalName = utils.ParseSPField(columnReference.name).id;
|
||||
if (!entity.__metadata__OriginalValues) { //SAVE orgininal values so we can undo;
|
||||
entity.__metadata__OriginalValues = _.cloneDeep(entity); // need deep if we have lookup values
|
||||
|
@ -403,7 +465,7 @@ class ListItemContainer extends React.Component<IListViewPageProps, IGridState>
|
|||
*/
|
||||
public ensureSiteUsers(siteUrl: string): SiteUsers {
|
||||
// see if the options are in the store, if so, return them, otherwoise dispatch an action to get them
|
||||
const siteUsers = _.find(this.props.siteUsers,x => {
|
||||
const siteUsers = _.find(this.props.siteUsers, x => {
|
||||
return (x.siteUrl === siteUrl);
|
||||
});
|
||||
if (siteUsers === undefined) {
|
||||
|
@ -418,7 +480,7 @@ class ListItemContainer extends React.Component<IListViewPageProps, IGridState>
|
|||
*/
|
||||
public getSiteUsers(siteUrl: string): SiteUsers {
|
||||
// see if the options are in the store, if so, return them, otherwoise dispatch an action to get them
|
||||
const siteUsers = _.find(this.props.siteUsers,x => {
|
||||
const siteUsers = _.find(this.props.siteUsers, x => {
|
||||
return (x.siteUrl === siteUrl);
|
||||
});
|
||||
return siteUsers;
|
||||
|
@ -429,7 +491,7 @@ class ListItemContainer extends React.Component<IListViewPageProps, IGridState>
|
|||
*/
|
||||
public ensureLookupOptions(lookupSite: string, lookupWebId: string, lookupListId: string, lookupField: string): LookupOptions {
|
||||
// see if the options are in the store, if so, return them, otherwoise dispatch an action to get them
|
||||
const lookupoptions =_.find( this.props.lookupOptions,x => {
|
||||
const lookupoptions = _.find(this.props.lookupOptions, x => {
|
||||
return (x.lookupField === lookupField) &&
|
||||
(x.lookupListId === lookupListId) &&
|
||||
(x.lookupSite === lookupSite) &&
|
||||
|
@ -447,7 +509,7 @@ class ListItemContainer extends React.Component<IListViewPageProps, IGridState>
|
|||
*/
|
||||
public getLookupOptions(lookupSite: string, lookupWebId: string, lookupListId: string, lookupField: string): LookupOptions {
|
||||
// see if the options are in the store, if so, return them, otherwoise dispatch an action to get them
|
||||
let lookupoptions = _.find(this.props.lookupOptions,x => {
|
||||
let lookupoptions = _.find(this.props.lookupOptions, x => {
|
||||
return (x.lookupField === lookupField) &&
|
||||
(x.lookupListId === lookupListId) &&
|
||||
(x.lookupSite === lookupSite) &&
|
||||
|
@ -463,7 +525,7 @@ class ListItemContainer extends React.Component<IListViewPageProps, IGridState>
|
|||
/** The id of the list definition to be retrieved */
|
||||
listdefid: string
|
||||
): ListDefinition {
|
||||
return _.find(this.props.listDefinitions,ld => ld.guid === listdefid);
|
||||
return _.find(this.props.listDefinitions, ld => ld.guid === listdefid);
|
||||
}
|
||||
/**
|
||||
* This method renders the contents of an individual cell in an editable format.
|
||||
|
@ -483,14 +545,20 @@ class ListItemContainer extends React.Component<IListViewPageProps, IGridState>
|
|||
// should I have a different handler for this?
|
||||
return (
|
||||
<Dropdown options={opts} selectedKey={entity.__metadata__ListDefinitionId} label=""
|
||||
onChanged={(selection: IDropdownOption) => { cellUpdated(selection); } } />
|
||||
onChanged={(selection: IDropdownOption) => { cellUpdated(selection); }} />
|
||||
);
|
||||
}
|
||||
const listDef = this.getListDefinition(entity.__metadata__ListDefinitionId);
|
||||
const colref = _.find(listDef.columnReferences,cr => cr.columnDefinitionId === column.guid);
|
||||
const colref = _.find(listDef.columnReferences, cr => cr.columnDefinitionId === column.guid);
|
||||
const internalName = utils.ParseSPField(colref.name).id;
|
||||
const columnValue = entity[internalName];
|
||||
switch (colref.fieldDefinition.TypeAsString) {
|
||||
case "Counter":// disable editting
|
||||
return (<span>
|
||||
{entity[internalName]}
|
||||
</span>
|
||||
);
|
||||
/* falls through */
|
||||
case "User":
|
||||
let siteUrl = listDef.siteUrl;
|
||||
let siteUsers = this.getSiteUsers(siteUrl);
|
||||
|
@ -502,7 +570,7 @@ class ListItemContainer extends React.Component<IListViewPageProps, IGridState>
|
|||
});
|
||||
const selectedKey = columnValue ? columnValue.Id : null;
|
||||
return (
|
||||
<Dropdown label="" options={options} selectedKey={selectedKey} onChanged={(selection: IDropdownOption) => { cellUpdated(selection); } } >
|
||||
<Dropdown label="" options={options} selectedKey={selectedKey} onChanged={(selection: IDropdownOption) => { cellUpdated(selection); }} >
|
||||
</Dropdown >
|
||||
);
|
||||
case SiteUsersStatus.fetching:
|
||||
|
@ -545,7 +613,7 @@ class ListItemContainer extends React.Component<IListViewPageProps, IGridState>
|
|||
return { key: opt.id, text: opt.value };
|
||||
});
|
||||
return (
|
||||
<Dropdown label="" options={options} selectedKey={(columnValue ? columnValue.Id : null)} onChanged={(selection: IDropdownOption) => { cellUpdated(selection); } } >
|
||||
<Dropdown label="" options={options} selectedKey={(columnValue ? columnValue.Id : null)} onChanged={(selection: IDropdownOption) => { cellUpdated(selection); }} >
|
||||
</Dropdown >
|
||||
);
|
||||
case LookupOptionStatus.fetching:
|
||||
|
@ -613,7 +681,7 @@ class ListItemContainer extends React.Component<IListViewPageProps, IGridState>
|
|||
return (
|
||||
<DatePicker strings={datpickerStrings} onSelectDate={cellUpdated} value={date}
|
||||
allowTextInput={true} isRequired={colref.fieldDefinition.Required}
|
||||
/>);
|
||||
/>);
|
||||
default:
|
||||
return (
|
||||
<input autoFocus type="text"
|
||||
|
@ -632,7 +700,8 @@ class ListItemContainer extends React.Component<IListViewPageProps, IGridState>
|
|||
const listDef = this.getListDefinition(entity.__metadata__ListDefinitionId);
|
||||
if (column.type === "__LISTDEFINITIONTITLE__") {// this type is sued to show the listdefinition name
|
||||
if (listDef != null) {//listdef has been selected
|
||||
return (<a href="#" onFocus={this.toggleEditing} style={{ textDecoration: "none" }} >
|
||||
return (<a href="#" onFocus={this.toggleEditing}
|
||||
style={{ textDecoration: (entity.__metadata__GridRowStatus === GridRowStatus.toBeDeleted) ? "line-through" : "none" }} >
|
||||
{listDef.listDefTitle}
|
||||
</a>);
|
||||
}
|
||||
|
@ -648,7 +717,7 @@ class ListItemContainer extends React.Component<IListViewPageProps, IGridState>
|
|||
</a>
|
||||
);
|
||||
}
|
||||
const colref =_.find( listDef.columnReferences,cr => cr.columnDefinitionId === column.guid);
|
||||
const colref = _.find(listDef.columnReferences, cr => cr.columnDefinitionId === column.guid);
|
||||
if (colref === undefined) { //Column has not been configured for this list
|
||||
return (<a href="#" onFocus={this.toggleEditing} style={{ textDecoration: "none" }} >
|
||||
'Column Not Defined'
|
||||
|
@ -661,12 +730,18 @@ class ListItemContainer extends React.Component<IListViewPageProps, IGridState>
|
|||
case "User":
|
||||
|
||||
if (entity[internalName] === undefined) { // value not set
|
||||
return (<a href="#" onFocus={this.toggleEditing} style={{ textDecoration: "none" }} >
|
||||
return (<a href="#" onFocus={this.toggleEditing}
|
||||
style={{ textDecoration: (entity.__metadata__GridRowStatus === GridRowStatus.toBeDeleted) ? "line-through" : "none" }}
|
||||
|
||||
>
|
||||
|
||||
</a>
|
||||
);
|
||||
} else {
|
||||
return (<a href="#" onFocus={this.toggleEditing} style={{ textDecoration: "none" }} >
|
||||
return (<a href="#" onFocus={this.toggleEditing}
|
||||
style={{ textDecoration: (entity.__metadata__GridRowStatus === GridRowStatus.toBeDeleted) ? "line-through" : "none" }}
|
||||
|
||||
>
|
||||
{entity[internalName]["Title"]}
|
||||
</a>
|
||||
);
|
||||
|
@ -675,32 +750,42 @@ class ListItemContainer extends React.Component<IListViewPageProps, IGridState>
|
|||
case "Lookup":
|
||||
|
||||
if (entity[internalName] === undefined) { // value not set
|
||||
return (<a href="#" onFocus={this.toggleEditing} style={{ textDecoration: "none" }} >
|
||||
return (<a href="#" onFocus={this.toggleEditing}
|
||||
style={{ textDecoration: (entity.__metadata__GridRowStatus === GridRowStatus.toBeDeleted) ? "line-through" : "none" }}
|
||||
>
|
||||
|
||||
</a>
|
||||
);
|
||||
} else {
|
||||
return (<a href="#" onFocus={this.toggleEditing} style={{ textDecoration: "none" }} >
|
||||
return (<a href="#" onFocus={this.toggleEditing}
|
||||
style={{ textDecoration: (entity.__metadata__GridRowStatus === GridRowStatus.toBeDeleted) ? "line-through" : "none" }}
|
||||
>
|
||||
{entity[internalName][colref.fieldDefinition.LookupField]}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
/* falls through */
|
||||
case "Text":
|
||||
return (<a href="#" onFocus={this.toggleEditing} style={{ textDecoration: "none" }} >
|
||||
return (<a href="#" onFocus={this.toggleEditing}
|
||||
style={{ textDecoration: (entity.__metadata__GridRowStatus === GridRowStatus.toBeDeleted) ? "line-through" : "none" }}
|
||||
>
|
||||
{entity[internalName]}
|
||||
</a>
|
||||
);
|
||||
/* falls through */
|
||||
case "Note":
|
||||
return (<a href="#" onFocus={this.toggleEditing} style={{ textDecoration: "none" }} dangerouslySetInnerHTML={{ __html: entity[internalName] }} >
|
||||
return (<a href="#" onFocus={this.toggleEditing}
|
||||
style={{ textDecoration: (entity.__metadata__GridRowStatus === GridRowStatus.toBeDeleted) ? "line-through" : "none" }}
|
||||
dangerouslySetInnerHTML={{ __html: entity[internalName] }} >
|
||||
</a>
|
||||
);
|
||||
/* falls through */
|
||||
case "DateTime":
|
||||
let value: string;
|
||||
if (entity[internalName] === null) {
|
||||
return (<a href="#" onFocus={this.toggleEditing} style={{ textDecoration: "none" }} >
|
||||
return (<a href="#" onFocus={this.toggleEditing}
|
||||
style={{ textDecoration: (entity.__metadata__GridRowStatus === GridRowStatus.toBeDeleted) ? "line-through" : "none" }}
|
||||
>
|
||||
|
||||
</a>);
|
||||
}
|
||||
|
@ -710,13 +795,23 @@ class ListItemContainer extends React.Component<IListViewPageProps, IGridState>
|
|||
else {
|
||||
value = entity[internalName];
|
||||
}
|
||||
return (<a href="#" onFocus={this.toggleEditing} style={{ textDecoration: "none" }} >
|
||||
return (<a href="#" onFocus={this.toggleEditing}
|
||||
style={{ textDecoration: (entity.__metadata__GridRowStatus === GridRowStatus.toBeDeleted) ? "line-through" : "none" }}
|
||||
>
|
||||
{value}
|
||||
</a>
|
||||
);
|
||||
/* falls through */
|
||||
case "Counter":// disable tabbing to field
|
||||
return (<span>
|
||||
{entity[internalName]}
|
||||
</span>
|
||||
);
|
||||
/* falls through */
|
||||
default:
|
||||
return (<a href="#" onFocus={this.toggleEditing} style={{ textDecoration: "none" }} >
|
||||
return (<a href="#" onFocus={this.toggleEditing}
|
||||
style={{ textDecoration: (entity.__metadata__GridRowStatus === GridRowStatus.toBeDeleted) ? "line-through" : "none" }}
|
||||
>
|
||||
{entity[internalName]}
|
||||
</a>
|
||||
);
|
||||
|
@ -747,6 +842,7 @@ class ListItemContainer extends React.Component<IListViewPageProps, IGridState>
|
|||
*/
|
||||
public TableRow(props: { entity: ListItem, columns: Array<ColumnDefinition>, cellUpdated: (newValue) => void, cellUpdatedEvent: (event: React.SyntheticEvent<any>) => void; }): JSX.Element {
|
||||
const {entity, columns, cellUpdated, cellUpdatedEvent} = props;
|
||||
|
||||
return (
|
||||
<tr>
|
||||
{
|
||||
|
@ -760,18 +856,22 @@ class ListItemContainer extends React.Component<IListViewPageProps, IGridState>
|
|||
<div>
|
||||
|
||||
<Button width="20" style={{ padding: 0 }}
|
||||
onClick={this.updateListItem} alt="Save to Sharepoint"
|
||||
onClick={this.handleUpdateListItemEvent} alt="Save to Sharepoint"
|
||||
buttonType={ButtonType.icon}
|
||||
icon="Save" disabled={!(entity.__metadata__OriginalValues)} />
|
||||
<Button width="20" style={{ padding: 0 }}
|
||||
{/*<Button width="20" style={{ padding: 0 }}
|
||||
onClick={this.removeListItem}
|
||||
buttonType={ButtonType.icon}
|
||||
icon="Delete" />*/}
|
||||
<Button width="20" style={{ padding: 0 }}
|
||||
onClick={this.markListItemAsDeleted}
|
||||
disabled={(entity.__metadata__GridRowStatus === GridRowStatus.toBeDeleted)}
|
||||
buttonType={ButtonType.icon}
|
||||
icon="Delete" />
|
||||
<Button width="20" style={{ padding: 0 }}
|
||||
// onClick={this.deleteList}
|
||||
buttonType={ButtonType.icon}
|
||||
disabled={!(entity.__metadata__OriginalValues)}
|
||||
onClick={this.undoItemChanges}
|
||||
disabled={(!(entity.__metadata__OriginalValues))}
|
||||
onClick={this.HandleUndoItemChangesEvent}
|
||||
icon="Undo" />
|
||||
</div>
|
||||
</td>
|
||||
|
@ -814,12 +914,14 @@ class ListItemContainer extends React.Component<IListViewPageProps, IGridState>
|
|||
{
|
||||
key: "Undo All changes",
|
||||
name: "UndoAll",
|
||||
icon: "Undo"
|
||||
icon: "Undo",
|
||||
onClick: this.undoAll
|
||||
},
|
||||
{
|
||||
key: "Save All ",
|
||||
name: "Save To SharePoint",
|
||||
icon: "Save"
|
||||
name: "Save All",
|
||||
icon: "Save",
|
||||
onClick: this.saveAll
|
||||
|
||||
}]} />
|
||||
|
||||
|
|
|
@ -3,7 +3,6 @@ import * as React from 'react';
|
|||
import * as ReactDom from 'react-dom';
|
||||
import {
|
||||
IPropertyPaneField,
|
||||
// IPropertyPaneFieldType,
|
||||
IPropertyPaneCustomFieldProps
|
||||
} from '@microsoft/sp-webpart-base';
|
||||
import PropertyFieldColumnDefinitionsHost, { IPropertyFieldColumnDefinitionsHostProps } from './PropertyFieldColumnDefinitionsHost';
|
||||
|
@ -12,11 +11,11 @@ export interface IPropertyFieldColumnDefinitionsProps {
|
|||
label: string;
|
||||
initialValue?: Array<ColumnDefinition>;
|
||||
onPropertyChange(propertyPath: string, oldValue: any, newValue: any): void;
|
||||
getColumnDefinitions: () =>Array<ColumnDefinition>;
|
||||
getColumnDefinitions: () => Array<ColumnDefinition>;
|
||||
}
|
||||
export interface IPropertyFieldColumnDefinitionsPropsInternal extends IPropertyPaneCustomFieldProps {
|
||||
label: string;
|
||||
initialValue?: Array<ColumnDefinition>;
|
||||
initialValue?: Array<ColumnDefinition>;
|
||||
targetProperty: string;
|
||||
onRender(elem: HTMLElement): void;
|
||||
onDispose(elem: HTMLElement): void;
|
||||
|
@ -34,7 +33,7 @@ class PropertyFieldColumnDefinitionsBuilder implements IPropertyPaneField<IPrope
|
|||
private onPropertyChange: (propertyPath: string, oldValue: any, newValue: any) => void;
|
||||
private customProperties: any;
|
||||
public constructor(_targetProperty: string, _properties: IPropertyFieldColumnDefinitionsPropsInternal) {
|
||||
this.render = this.render.bind(this);
|
||||
this.render = this.render.bind(this);
|
||||
this.properties = _properties;
|
||||
this.label = _properties.label;
|
||||
this.properties.onDispose = this.dispose;
|
||||
|
@ -45,8 +44,8 @@ class PropertyFieldColumnDefinitionsBuilder implements IPropertyPaneField<IPrope
|
|||
private render(elem: HTMLElement): void {
|
||||
const element: React.ReactElement<IPropertyFieldColumnDefinitionsHostProps> = React.createElement(PropertyFieldColumnDefinitionsHost, {
|
||||
label: this.label,
|
||||
onPropertyChange: this.onPropertyChange,
|
||||
columnDefinitions: this.customProperties,
|
||||
onPropertyChange: this.onPropertyChange,
|
||||
columnDefinitions: this.customProperties,
|
||||
|
||||
});
|
||||
ReactDom.render(element, elem);
|
||||
|
@ -56,6 +55,7 @@ class PropertyFieldColumnDefinitionsBuilder implements IPropertyPaneField<IPrope
|
|||
}
|
||||
export function PropertyFieldColumnDefinitions(targetProperty: string, properties: IPropertyFieldColumnDefinitionsProps): IPropertyPaneField<IPropertyFieldColumnDefinitionsPropsInternal> {
|
||||
|
||||
|
||||
//Create an internal properties object from the given properties
|
||||
var newProperties: IPropertyFieldColumnDefinitionsPropsInternal = {
|
||||
label: properties.label,
|
||||
|
@ -70,6 +70,7 @@ export function PropertyFieldColumnDefinitions(targetProperty: string, propertie
|
|||
//Calles the PropertyFieldColumnDefinitions builder object
|
||||
//This object will simulate a PropertyFieldCustom to manage his rendering process
|
||||
return new PropertyFieldColumnDefinitionsBuilder(targetProperty, newProperties);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@ class PropertyFieldListDefinitionsBuilder implements IPropertyPaneField<IPropert
|
|||
//Custom properties
|
||||
private label: string;
|
||||
private onPropertyChange: (propertyPath: string, oldValue: any, newValue: any) => void;
|
||||
private customProperties: any;
|
||||
|
||||
public constructor(_targetProperty: string, _properties: IPropertyFieldListDefinitionsPropsInternal) {
|
||||
|
||||
this.render = this.render.bind(this);
|
||||
|
|
|
@ -3,11 +3,10 @@ import {
|
|||
MessageBar,
|
||||
MessageBarType
|
||||
} from "office-ui-fabric-react";
|
||||
//const connect = require("react-redux").connect;
|
||||
import * as redux from "react-redux";
|
||||
const connect = redux.connect;
|
||||
import SystemStatus from "../model/SystemStatus";
|
||||
import Content from "../components/content";
|
||||
import ListItemContainer from "./ListItemContainer";
|
||||
interface IAppProps extends React.Props<any> {
|
||||
systemStatus: SystemStatus;
|
||||
}
|
||||
|
@ -35,15 +34,13 @@ class App extends React.Component<IAppProps, void> {
|
|||
const { children} = this.props;
|
||||
return (
|
||||
<div>
|
||||
|
||||
<div>
|
||||
{this.messageBar(this.props.systemStatus.fetchStatus)}
|
||||
<div>{this.props.systemStatus.currentAction}
|
||||
</div>
|
||||
</div>
|
||||
<Content isVisible={true}>
|
||||
{children}
|
||||
</Content>
|
||||
<ListItemContainer>
|
||||
</ListItemContainer>
|
||||
</div >
|
||||
);
|
||||
};
|
||||
|
|
|
@ -12,7 +12,9 @@ define([], function () {
|
|||
"ListDefinitionFieldLabel": "List Definitions",
|
||||
"ListDefinitionsButtonSelect": "Update Lists",
|
||||
"ListDefinitionsButtonReset": "Clear",
|
||||
"ListDefinitionsTitle": "List Definitions"
|
||||
"ListDefinitionsTitle": "List Definitions",
|
||||
|
||||
"WebSelectorHeaderText": "Select the Web containging the list"
|
||||
|
||||
}
|
||||
});
|
|
@ -17,6 +17,8 @@ declare interface ISpfxReactGridStrings {
|
|||
ColumnDefinitionsButtonReset:string;
|
||||
/**The Title on the popupPage */
|
||||
ColumnDefinitionsTitle:string;
|
||||
/**The Title on the WebSelector panel */
|
||||
WebSelectorHeaderText:string;
|
||||
}
|
||||
declare module 'spfxReactGridStrings' {
|
||||
const strings: ISpfxReactGridStrings;
|
||||
|
|
|
@ -1,10 +1,19 @@
|
|||
export default class ColumnDefinition {
|
||||
export enum SortDirection {
|
||||
None,
|
||||
Ascending,
|
||||
Descending
|
||||
}
|
||||
export default class ColumnDefinition {
|
||||
|
||||
public constructor(
|
||||
public guid: string,
|
||||
public name: string,
|
||||
public width: number,
|
||||
public editable: boolean = true,
|
||||
public type: string = "Text",
|
||||
public sortDirection:SortDirection = SortDirection.None,
|
||||
public sortSequence:Number=0
|
||||
)
|
||||
{ }
|
||||
|
||||
}
|
||||
|
|
|
@ -6,5 +6,5 @@ enum GridRowStatus{
|
|||
toBeDeleted
|
||||
|
||||
}
|
||||
/**Status of row )mew */
|
||||
/**Status of row */
|
||||
export default GridRowStatus;
|
|
@ -1,4 +1,8 @@
|
|||
import ListItem from "../Model/ListItem";
|
||||
import ColumnDefinition from "../Model/ColumnDefinition";
|
||||
import { SortDirection } from "../Model/ColumnDefinition";
|
||||
import ListDefinition from "../Model/ListDefinition";
|
||||
import * as utils from "../utils/utils";
|
||||
import * as _ from "lodash";
|
||||
import {
|
||||
ADD_LISTITEM,
|
||||
|
@ -74,7 +78,68 @@ function saveListItem(state: Array<ListItem>, action: { payload: { listItem: Lis
|
|||
}
|
||||
return newarray2;
|
||||
}
|
||||
function gotListItems(state: Array<ListItem>, action: { payload: { items: Array<ListItem>, listDefinitions: Array<ListDefinition>, columnDefinitions: Array<ColumnDefinition> } }) {
|
||||
/** Do Initial Sort here; */
|
||||
|
||||
const sortableColumns = _.filter(action.payload.columnDefinitions, cd => {
|
||||
const x = (cd.sortDirection !== SortDirection.None);
|
||||
return x;
|
||||
});
|
||||
const sortColumns = _.sortBy(sortableColumns, cd => {
|
||||
return cd.sortSequence;
|
||||
});
|
||||
// so a given grid column may be mapped to FieldA in list1 and FieldB in list2, need to find the fields to compare , then compare them
|
||||
const results = _.union(state, action.payload.items)
|
||||
.sort((listItem1: ListItem, listItem2: ListItem): number => {
|
||||
const listDefinition1 = _.find(action.payload.listDefinitions, ld => { return ld.guid === listItem1.__metadata__ListDefinitionId ;});
|
||||
const listDefinition2 = _.find(action.payload.listDefinitions, ld => { return ld.guid === listItem2.__metadata__ListDefinitionId ;});
|
||||
for (const sortColumn of sortColumns) {
|
||||
debugger;
|
||||
if (sortColumn.type === "__LISTDEFINITIONTITLE__") {
|
||||
if (listDefinition1.listDefTitle === listDefinition2.listDefTitle) {
|
||||
return 0;
|
||||
}
|
||||
if (sortColumn.sortDirection === SortDirection.Ascending) {
|
||||
if (listDefinition1.listDefTitle <= listDefinition2.listDefTitle) {
|
||||
return -1;
|
||||
} else {
|
||||
return +1;
|
||||
}
|
||||
} else {
|
||||
if (listDefinition1.listDefTitle >= listDefinition2.listDefTitle) {
|
||||
return -1;
|
||||
} else {
|
||||
return +1;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
const columnId = sortColumn.guid;
|
||||
const list1Column = _.find(listDefinition1.columnReferences, cr => { return cr.columnDefinitionId = columnId; }).name; // internalname#;Displayname
|
||||
const list1ColumnName = utils.ParseSPField(list1Column).id; // internalname
|
||||
const list2Column = _.find(listDefinition2.columnReferences, cr => { return cr.columnDefinitionId = columnId; }).name;
|
||||
const list2ColumnName = utils.ParseSPField(list2Column).id;
|
||||
|
||||
if (sortColumn.sortDirection === SortDirection.Ascending) {
|
||||
if (listItem1[list1ColumnName] <= listItem2[list2ColumnName]) {
|
||||
return -1;
|
||||
} else {
|
||||
return +1;
|
||||
}
|
||||
} else {
|
||||
if (listItem1[list1ColumnName] >= listItem2[list2ColumnName]) {
|
||||
return -1;
|
||||
} else {
|
||||
return +1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
// return _.union(state, action.payload.items);
|
||||
return results;
|
||||
}
|
||||
function listItemReducer(state = INITIAL_STATE, action: any = { type: "" }) {
|
||||
switch (action.type) {
|
||||
case ADD_LISTITEM:
|
||||
|
@ -90,7 +155,7 @@ function listItemReducer(state = INITIAL_STATE, action: any = { type: "" }) {
|
|||
case UNDO_LISTITEMCHANGES:
|
||||
return undoListItemChanges(state, action);
|
||||
case GOT_LISTITEMS:
|
||||
return _.union(state, action.payload.items);
|
||||
return gotListItems(state, action);
|
||||
case CLEAR_LISTITEMS:
|
||||
return [];
|
||||
default:
|
||||
|
|
|
@ -7,7 +7,6 @@ import LookupOptionsReducer from "./LookupOptionsReducer";
|
|||
import SiteReducer from "./SiteReducer";
|
||||
import SiteUserReducer from "./SiteUsersReducer";
|
||||
import SystemStatus from "./SystemStatus";
|
||||
import { routerReducer } from "react-router-redux";
|
||||
export function RootReducer(state, action) {
|
||||
const combinedReducers = combineReducers(
|
||||
{
|
||||
|
@ -16,7 +15,6 @@ export function RootReducer(state, action) {
|
|||
columns: ColumnReducer,
|
||||
sites: SiteReducer,
|
||||
pageContext: PageContextReducer,
|
||||
routing: routerReducer,
|
||||
systemStatus:SystemStatus,
|
||||
lookupOptions:LookupOptionsReducer,
|
||||
siteUsers:SiteUserReducer
|
||||
|
|
|
@ -5,10 +5,8 @@ import {
|
|||
Middleware
|
||||
} from "redux";
|
||||
import { fromJS } from "immutable";
|
||||
import { createMemoryHistory } from "react-router";
|
||||
import { routerMiddleware } from "react-router-redux";
|
||||
import thunk from "redux-thunk";
|
||||
import {Store} from "redux"
|
||||
import {Store} from "redux";
|
||||
import promiseMiddleware from "redux-promise-middleware";
|
||||
|
||||
import { RootReducer } from "../reducers/rootReducer";
|
||||
|
@ -28,9 +26,8 @@ function configureStore(initialState) {
|
|||
}
|
||||
|
||||
function _getMiddleware(): Middleware[] {
|
||||
const history = createMemoryHistory();
|
||||
|
||||
let middleware = [
|
||||
routerMiddleware(history),
|
||||
promiseMiddleware(),
|
||||
thunk,
|
||||
];
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
import * as React from "react";
|
||||
//const { IndexRoute, Route } = require("react-router");
|
||||
import { IndexRoute, Route } from "react-router";
|
||||
import App from "../containers/app";
|
||||
import ListDefinitionContainer from "../containers/ListDefinitionContainer";
|
||||
import ColumnDefinitionContainer from "../containers/ColumnDefinitionContainer";
|
||||
import ListItemContainer from "../containers/ListItemContainer";
|
||||
export default (
|
||||
<Route path="/" component={App}>
|
||||
<IndexRoute component={ListItemContainer} />
|
||||
<Route path="/lists" component={ListDefinitionContainer}>
|
||||
</Route>
|
||||
<Route path="/columns" component={ColumnDefinitionContainer}>
|
||||
</Route>
|
||||
|
||||
</Route>
|
||||
);
|
|
@ -4,11 +4,9 @@
|
|||
"redux-promise-middleware": "registry:dt/redux-promise-middleware#0.0.0+20160108032528"
|
||||
},
|
||||
"dependencies": {
|
||||
"react-router": "registry:npm/react-router#2.4.0+20160628165748",
|
||||
"react-router-redux": "registry:npm/react-router-redux#4.0.0+20160602212457",
|
||||
"redux-thunk": "registry:npm/redux-thunk#2.0.0+20160525185520"
|
||||
},
|
||||
"devDependencies": {
|
||||
"react-redux": "registry:npm/react-redux#4.4.0+20160614222153"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
### 0.6.5 / 2016-05-20
|
||||
|
||||
* Don't mutate buffers passed in by the application when masking
|
||||
|
||||
### 0.6.4 / 2016-01-07
|
||||
|
||||
* If a number is given as input for a frame payload, send it as a string
|
||||
|
||||
### 0.6.3 / 2015-11-06
|
||||
|
||||
* Reject draft-76 handshakes if their Sec-WebSocket-Key headers are invalid
|
||||
* Throw a more helpful error if a client is created with an invalid URL
|
||||
|
||||
### 0.6.2 / 2015-07-18
|
||||
|
||||
* When the peer sends a close frame with no error code, emit 1000
|
||||
|
||||
### 0.6.1 / 2015-07-13
|
||||
|
||||
* Use the `buffer.{read,write}UInt{16,32}BE` methods for reading/writing numbers
|
||||
to buffers rather than including duplicate logic for this
|
||||
|
||||
### 0.6.0 / 2015-07-08
|
||||
|
||||
* Allow the parser to recover cleanly if event listeners raise an error
|
||||
* Add a `pong` method for sending unsolicited pong frames
|
||||
|
||||
### 0.5.4 / 2015-03-29
|
||||
|
||||
* Don't emit extra close frames if we receive a close frame after we already
|
||||
sent one
|
||||
* Fail the connection when the driver receives an invalid
|
||||
`Sec-WebSocket-Extensions` header
|
||||
|
||||
### 0.5.3 / 2015-02-22
|
||||
|
||||
* Don't treat incoming data as WebSocket frames if a client driver is closed
|
||||
before receiving the server handshake
|
||||
|
||||
### 0.5.2 / 2015-02-19
|
||||
|
||||
* Fix compatibility with the HTTP parser on io.js
|
||||
* Use `websocket-extensions` to make sure messages and close frames are kept in
|
||||
order
|
||||
* Don't emit multiple `error` events
|
||||
|
||||
### 0.5.1 / 2014-12-18
|
||||
|
||||
* Don't allow drivers to be created with unrecognized options
|
||||
|
||||
### 0.5.0 / 2014-12-13
|
||||
|
||||
* Support protocol extensions via the websocket-extensions module
|
||||
|
||||
### 0.4.0 / 2014-11-08
|
||||
|
||||
* Support connection via HTTP proxies using `CONNECT`
|
||||
|
||||
### 0.3.6 / 2014-10-04
|
||||
|
||||
* It is now possible to call `close()` before `start()` and close the driver
|
||||
|
||||
### 0.3.5 / 2014-07-06
|
||||
|
||||
* Don't hold references to frame buffers after a message has been emitted
|
||||
* Make sure that `protocol` and `version` are exposed properly by the TCP driver
|
||||
|
||||
### 0.3.4 / 2014-05-08
|
||||
|
||||
* Don't hold memory-leaking references to I/O buffers after they have been
|
||||
parsed
|
||||
|
||||
### 0.3.3 / 2014-04-24
|
||||
|
||||
* Correct the draft-76 status line reason phrase
|
||||
|
||||
### 0.3.2 / 2013-12-29
|
||||
|
||||
* Expand `maxLength` to cover sequences of continuation frames and
|
||||
`draft-{75,76}`
|
||||
* Decrease default maximum frame buffer size to 64MB
|
||||
* Stop parsing when the protocol enters a failure mode, to save CPU cycles
|
||||
|
||||
### 0.3.1 / 2013-12-03
|
||||
|
||||
* Add a `maxLength` option to limit allowed frame size
|
||||
* Don't pre-allocate a message buffer until the whole frame has arrived
|
||||
* Fix compatibility with Node v0.11 `HTTPParser`
|
||||
|
||||
### 0.3.0 / 2013-09-09
|
||||
|
||||
* Support client URLs with Basic Auth credentials
|
||||
|
||||
### 0.2.2 / 2013-07-05
|
||||
|
||||
* No functional changes, just updates to package.json
|
||||
|
||||
### 0.2.1 / 2013-05-17
|
||||
|
||||
* Export the isSecureRequest() method since faye-websocket relies on it
|
||||
* Queue sent messages in the client's initial state
|
||||
|
||||
### 0.2.0 / 2013-05-12
|
||||
|
||||
* Add API for setting and reading headers
|
||||
* Add Driver.server() method for getting a driver for TCP servers
|
||||
|
||||
### 0.1.0 / 2013-05-04
|
||||
|
||||
* First stable release
|
|
@ -0,0 +1,4 @@
|
|||
# Code of Conduct
|
||||
|
||||
All projects under the [Faye](https://github.com/faye) umbrella are covered by
|
||||
the [Code of Conduct](https://github.com/faye/code-of-conduct).
|
|
@ -0,0 +1,383 @@
|
|||
# websocket-driver [![Build Status](https://travis-ci.org/faye/websocket-driver-node.svg)](https://travis-ci.org/faye/websocket-driver-node)
|
||||
|
||||
This module provides a complete implementation of the WebSocket protocols that
|
||||
can be hooked up to any I/O stream. It aims to simplify things by decoupling the
|
||||
protocol details from the I/O layer, such that users only need to implement code
|
||||
to stream data in and out of it without needing to know anything about how the
|
||||
protocol actually works. Think of it as a complete WebSocket system with
|
||||
pluggable I/O.
|
||||
|
||||
Due to this design, you get a lot of things for free. In particular, if you hook
|
||||
this module up to some I/O object, it will do all of this for you:
|
||||
|
||||
* Select the correct server-side driver to talk to the client
|
||||
* Generate and send both server- and client-side handshakes
|
||||
* Recognize when the handshake phase completes and the WS protocol begins
|
||||
* Negotiate subprotocol selection based on `Sec-WebSocket-Protocol`
|
||||
* Negotiate and use extensions via the
|
||||
[websocket-extensions](https://github.com/faye/websocket-extensions-node)
|
||||
module
|
||||
* Buffer sent messages until the handshake process is finished
|
||||
* Deal with proxies that defer delivery of the draft-76 handshake body
|
||||
* Notify you when the socket is open and closed and when messages arrive
|
||||
* Recombine fragmented messages
|
||||
* Dispatch text, binary, ping, pong and close frames
|
||||
* Manage the socket-closing handshake process
|
||||
* Automatically reply to ping frames with a matching pong
|
||||
* Apply masking to messages sent by the client
|
||||
|
||||
This library was originally extracted from the [Faye](http://faye.jcoglan.com)
|
||||
project but now aims to provide simple WebSocket support for any Node-based
|
||||
project.
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
```
|
||||
$ npm install websocket-driver
|
||||
```
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
This module provides protocol drivers that have the same interface on the server
|
||||
and on the client. A WebSocket driver is an object with two duplex streams
|
||||
attached; one for incoming/outgoing messages and one for managing the wire
|
||||
protocol over an I/O stream. The full API is described below.
|
||||
|
||||
|
||||
### Server-side with HTTP
|
||||
|
||||
A Node webserver emits a special event for 'upgrade' requests, and this is where
|
||||
you should handle WebSockets. You first check whether the request is a
|
||||
WebSocket, and if so you can create a driver and attach the request's I/O stream
|
||||
to it.
|
||||
|
||||
```js
|
||||
var http = require('http'),
|
||||
websocket = require('websocket-driver');
|
||||
|
||||
var server = http.createServer();
|
||||
|
||||
server.on('upgrade', function(request, socket, body) {
|
||||
if (!websocket.isWebSocket(request)) return;
|
||||
|
||||
var driver = websocket.http(request);
|
||||
|
||||
driver.io.write(body);
|
||||
socket.pipe(driver.io).pipe(socket);
|
||||
|
||||
driver.messages.on('data', function(message) {
|
||||
console.log('Got a message', message);
|
||||
});
|
||||
|
||||
driver.start();
|
||||
});
|
||||
```
|
||||
|
||||
Note the line `driver.io.write(body)` - you must pass the `body` buffer to the
|
||||
socket driver in order to make certain versions of the protocol work.
|
||||
|
||||
|
||||
### Server-side with TCP
|
||||
|
||||
You can also handle WebSocket connections in a bare TCP server, if you're not
|
||||
using an HTTP server and don't want to implement HTTP parsing yourself.
|
||||
|
||||
The driver will emit a `connect` event when a request is received, and at this
|
||||
point you can detect whether it's a WebSocket and handle it as such. Here's an
|
||||
example using the Node `net` module:
|
||||
|
||||
```js
|
||||
var net = require('net'),
|
||||
websocket = require('websocket-driver');
|
||||
|
||||
var server = net.createServer(function(connection) {
|
||||
var driver = websocket.server();
|
||||
|
||||
driver.on('connect', function() {
|
||||
if (websocket.isWebSocket(driver)) {
|
||||
driver.start();
|
||||
} else {
|
||||
// handle other HTTP requests
|
||||
}
|
||||
});
|
||||
|
||||
driver.on('close', function() { connection.end() });
|
||||
connection.on('error', function() {});
|
||||
|
||||
connection.pipe(driver.io).pipe(connection);
|
||||
|
||||
driver.messages.pipe(driver.messages);
|
||||
});
|
||||
|
||||
server.listen(4180);
|
||||
```
|
||||
|
||||
In the `connect` event, the driver gains several properties to describe the
|
||||
request, similar to a Node request object, such as `method`, `url` and
|
||||
`headers`. However you should remember it's not a real request object; you
|
||||
cannot write data to it, it only tells you what request data we parsed from the
|
||||
input.
|
||||
|
||||
If the request has a body, it will be in the `driver.body` buffer, but only as
|
||||
much of the body as has been piped into the driver when the `connect` event
|
||||
fires.
|
||||
|
||||
|
||||
### Client-side
|
||||
|
||||
Similarly, to implement a WebSocket client you just need to make a driver by
|
||||
passing in a URL. After this you use the driver API as described below to
|
||||
process incoming data and send outgoing data.
|
||||
|
||||
|
||||
```js
|
||||
var net = require('net'),
|
||||
websocket = require('websocket-driver');
|
||||
|
||||
var driver = websocket.client('ws://www.example.com/socket'),
|
||||
tcp = net.connect(80, 'www.example.com');
|
||||
|
||||
tcp.pipe(driver.io).pipe(tcp);
|
||||
|
||||
tcp.on('connect', function() {
|
||||
driver.start();
|
||||
});
|
||||
|
||||
driver.messages.on('data', function(message) {
|
||||
console.log('Got a message', message);
|
||||
});
|
||||
```
|
||||
|
||||
Client drivers have two additional properties for reading the HTTP data that was
|
||||
sent back by the server:
|
||||
|
||||
* `driver.statusCode` - the integer value of the HTTP status code
|
||||
* `driver.headers` - an object containing the response headers
|
||||
|
||||
|
||||
### HTTP Proxies
|
||||
|
||||
The client driver supports connections via HTTP proxies using the `CONNECT`
|
||||
method. Instead of sending the WebSocket handshake immediately, it will send a
|
||||
`CONNECT` request, wait for a `200` response, and then proceed as normal.
|
||||
|
||||
To use this feature, call `driver.proxy(url)` where `url` is the origin of the
|
||||
proxy, including a username and password if required. This produces a duplex
|
||||
stream that you should pipe in and out of your TCP connection to the proxy
|
||||
server. When the proxy emits `connect`, you can then pipe `driver.io` to your
|
||||
TCP stream and call `driver.start()`.
|
||||
|
||||
```js
|
||||
var net = require('net'),
|
||||
websocket = require('websocket-driver');
|
||||
|
||||
var driver = websocket.client('ws://www.example.com/socket'),
|
||||
proxy = driver.proxy('http://username:password@proxy.example.com'),
|
||||
tcp = net.connect(80, 'proxy.example.com');
|
||||
|
||||
tcp.pipe(proxy).pipe(tcp, {end: false});
|
||||
|
||||
tcp.on('connect', function() {
|
||||
proxy.start();
|
||||
});
|
||||
|
||||
proxy.on('connect', function() {
|
||||
driver.io.pipe(tcp).pipe(driver.io);
|
||||
driver.start();
|
||||
});
|
||||
|
||||
driver.messages.on('data', function(message) {
|
||||
console.log('Got a message', message);
|
||||
});
|
||||
```
|
||||
|
||||
The proxy's `connect` event is also where you should perform a TLS handshake on
|
||||
your TCP stream, if you are connecting to a `wss:` endpoint.
|
||||
|
||||
In the event that proxy connection fails, `proxy` will emit an `error`. You can
|
||||
inspect the proxy's response via `proxy.statusCode` and `proxy.headers`.
|
||||
|
||||
```js
|
||||
proxy.on('error', function(error) {
|
||||
console.error(error.message);
|
||||
console.log(proxy.statusCode);
|
||||
console.log(proxy.headers);
|
||||
});
|
||||
```
|
||||
|
||||
Before calling `proxy.start()` you can set custom headers using
|
||||
`proxy.setHeader()`:
|
||||
|
||||
```js
|
||||
proxy.setHeader('User-Agent', 'node');
|
||||
proxy.start();
|
||||
```
|
||||
|
||||
|
||||
### Driver API
|
||||
|
||||
Drivers are created using one of the following methods:
|
||||
|
||||
```js
|
||||
driver = websocket.http(request, options)
|
||||
driver = websocket.server(options)
|
||||
driver = websocket.client(url, options)
|
||||
```
|
||||
|
||||
The `http` method returns a driver chosen using the headers from a Node HTTP
|
||||
request object. The `server` method returns a driver that will parse an HTTP
|
||||
request and then decide which driver to use for it using the `http` method. The
|
||||
`client` method always returns a driver for the RFC version of the protocol with
|
||||
masking enabled on outgoing frames.
|
||||
|
||||
The `options` argument is optional, and is an object. It may contain the
|
||||
following fields:
|
||||
|
||||
* `maxLength` - the maximum allowed size of incoming message frames, in bytes.
|
||||
The default value is `2^26 - 1`, or 1 byte short of 64 MiB.
|
||||
* `protocols` - an array of strings representing acceptable subprotocols for use
|
||||
over the socket. The driver will negotiate one of these to use via the
|
||||
`Sec-WebSocket-Protocol` header if supported by the other peer.
|
||||
|
||||
A driver has two duplex streams attached to it:
|
||||
|
||||
* <b>`driver.io`</b> - this stream should be attached to an I/O socket like a
|
||||
TCP stream. Pipe incoming TCP chunks to this stream for them to be parsed, and
|
||||
pipe this stream back into TCP to send outgoing frames.
|
||||
* <b>`driver.messages`</b> - this stream emits messages received over the
|
||||
WebSocket. Writing to it sends messages to the other peer by emitting frames
|
||||
via the `driver.io` stream.
|
||||
|
||||
All drivers respond to the following API methods, but some of them are no-ops
|
||||
depending on whether the client supports the behaviour.
|
||||
|
||||
Note that most of these methods are commands: if they produce data that should
|
||||
be sent over the socket, they will give this to you by emitting `data` events on
|
||||
the `driver.io` stream.
|
||||
|
||||
#### `driver.on('open', function(event) {})`
|
||||
|
||||
Adds a callback to execute when the socket becomes open.
|
||||
|
||||
#### `driver.on('message', function(event) {})`
|
||||
|
||||
Adds a callback to execute when a message is received. `event` will have a
|
||||
`data` attribute containing either a string in the case of a text message or a
|
||||
`Buffer` in the case of a binary message.
|
||||
|
||||
You can also listen for messages using the `driver.messages.on('data')` event,
|
||||
which emits strings for text messages and buffers for binary messages.
|
||||
|
||||
#### `driver.on('error', function(event) {})`
|
||||
|
||||
Adds a callback to execute when a protocol error occurs due to the other peer
|
||||
sending an invalid byte sequence. `event` will have a `message` attribute
|
||||
describing the error.
|
||||
|
||||
#### `driver.on('close', function(event) {})`
|
||||
|
||||
Adds a callback to execute when the socket becomes closed. The `event` object
|
||||
has `code` and `reason` attributes.
|
||||
|
||||
#### `driver.addExtension(extension)`
|
||||
|
||||
Registers a protocol extension whose operation will be negotiated via the
|
||||
`Sec-WebSocket-Extensions` header. `extension` is any extension compatible with
|
||||
the [websocket-extensions](https://github.com/faye/websocket-extensions-node)
|
||||
framework.
|
||||
|
||||
#### `driver.setHeader(name, value)`
|
||||
|
||||
Sets a custom header to be sent as part of the handshake response, either from
|
||||
the server or from the client. Must be called before `start()`, since this is
|
||||
when the headers are serialized and sent.
|
||||
|
||||
#### `driver.start()`
|
||||
|
||||
Initiates the protocol by sending the handshake - either the response for a
|
||||
server-side driver or the request for a client-side one. This should be the
|
||||
first method you invoke. Returns `true` if and only if a handshake was sent.
|
||||
|
||||
#### `driver.parse(string)`
|
||||
|
||||
Takes a string and parses it, potentially resulting in message events being
|
||||
emitted (see `on('message')` above) or in data being sent to `driver.io`. You
|
||||
should send all data you receive via I/O to this method by piping a stream into
|
||||
`driver.io`.
|
||||
|
||||
#### `driver.text(string)`
|
||||
|
||||
Sends a text message over the socket. If the socket handshake is not yet
|
||||
complete, the message will be queued until it is. Returns `true` if the message
|
||||
was sent or queued, and `false` if the socket can no longer send messages.
|
||||
|
||||
This method is equivalent to `driver.messages.write(string)`.
|
||||
|
||||
#### `driver.binary(buffer)`
|
||||
|
||||
Takes a `Buffer` and sends it as a binary message. Will queue and return `true`
|
||||
or `false` the same way as the `text` method. It will also return `false` if the
|
||||
driver does not support binary messages.
|
||||
|
||||
This method is equivalent to `driver.messages.write(buffer)`.
|
||||
|
||||
#### `driver.ping(string = '', function() {})`
|
||||
|
||||
Sends a ping frame over the socket, queueing it if necessary. `string` and the
|
||||
callback are both optional. If a callback is given, it will be invoked when the
|
||||
socket receives a pong frame whose content matches `string`. Returns `false` if
|
||||
frames can no longer be sent, or if the driver does not support ping/pong.
|
||||
|
||||
#### `driver.pong(string = '')`
|
||||
|
||||
Sends a pong frame over the socket, queueing it if necessary. `string` is
|
||||
optional. Returns `false` if frames can no longer be sent, or if the driver does
|
||||
not support ping/pong.
|
||||
|
||||
You don't need to call this when a ping frame is received; pings are replied to
|
||||
automatically by the driver. This method is for sending unsolicited pongs.
|
||||
|
||||
#### `driver.close()`
|
||||
|
||||
Initiates the closing handshake if the socket is still open. For drivers with no
|
||||
closing handshake, this will result in the immediate execution of the
|
||||
`on('close')` driver. For drivers with a closing handshake, this sends a closing
|
||||
frame and `emit('close')` will execute when a response is received or a protocol
|
||||
error occurs.
|
||||
|
||||
#### `driver.version`
|
||||
|
||||
Returns the WebSocket version in use as a string. Will either be `hixie-75`,
|
||||
`hixie-76` or `hybi-$version`.
|
||||
|
||||
#### `driver.protocol`
|
||||
|
||||
Returns a string containing the selected subprotocol, if any was agreed upon
|
||||
using the `Sec-WebSocket-Protocol` mechanism. This value becomes available after
|
||||
`emit('open')` has fired.
|
||||
|
||||
|
||||
## License
|
||||
|
||||
(The MIT License)
|
||||
|
||||
Copyright (c) 2010-2016 James Coglan
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the 'Software'), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@ -0,0 +1,22 @@
|
|||
var net = require('net'),
|
||||
websocket = require('..'),
|
||||
deflate = require('permessage-deflate');
|
||||
|
||||
var server = net.createServer(function(connection) {
|
||||
var driver = websocket.server();
|
||||
driver.addExtension(deflate);
|
||||
|
||||
driver.on('connect', function() {
|
||||
if (websocket.isWebSocket(driver)) driver.start();
|
||||
});
|
||||
|
||||
driver.on('close', function() { connection.end() });
|
||||
connection.on('error', function() {});
|
||||
|
||||
connection.pipe(driver.io);
|
||||
driver.io.pipe(connection);
|
||||
|
||||
driver.messages.pipe(driver.messages);
|
||||
});
|
||||
|
||||
server.listen(process.argv[2]);
|
|
@ -0,0 +1,95 @@
|
|||
{
|
||||
"_args": [
|
||||
[
|
||||
{
|
||||
"name": "websocket-driver",
|
||||
"raw": "websocket-driver@>=0.5.1",
|
||||
"rawSpec": ">=0.5.1",
|
||||
"scope": null,
|
||||
"spec": ">=0.5.1",
|
||||
"type": "range"
|
||||
},
|
||||
"C:\\Users\\trwg1\\Documents\\GitHub\\sp-dev-fx-webparts3\\samples\\react-multilist-grid\\node_modules\\faye-websocket"
|
||||
]
|
||||
],
|
||||
"_from": "websocket-driver@>=0.5.1",
|
||||
"_id": "websocket-driver@0.6.5",
|
||||
"_inCache": true,
|
||||
"_installable": true,
|
||||
"_location": "/websocket-driver",
|
||||
"_nodeVersion": "4.4.4",
|
||||
"_npmOperationalInternal": {
|
||||
"host": "packages-12-west.internal.npmjs.com",
|
||||
"tmp": "tmp/websocket-driver-0.6.5.tgz_1463730072239_0.9899731166660786"
|
||||
},
|
||||
"_npmUser": {
|
||||
"email": "jcoglan@gmail.com",
|
||||
"name": "jcoglan"
|
||||
},
|
||||
"_npmVersion": "2.15.1",
|
||||
"_phantomChildren": {},
|
||||
"_requested": {
|
||||
"name": "websocket-driver",
|
||||
"raw": "websocket-driver@>=0.5.1",
|
||||
"rawSpec": ">=0.5.1",
|
||||
"scope": null,
|
||||
"spec": ">=0.5.1",
|
||||
"type": "range"
|
||||
},
|
||||
"_requiredBy": [
|
||||
"/faye-websocket"
|
||||
],
|
||||
"_resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.6.5.tgz",
|
||||
"_shasum": "5cb2556ceb85f4373c6d8238aa691c8454e13a36",
|
||||
"_shrinkwrap": null,
|
||||
"_spec": "websocket-driver@>=0.5.1",
|
||||
"_where": "C:\\Users\\trwg1\\Documents\\GitHub\\sp-dev-fx-webparts3\\samples\\react-multilist-grid\\node_modules\\faye-websocket",
|
||||
"author": {
|
||||
"email": "jcoglan@gmail.com",
|
||||
"name": "James Coglan",
|
||||
"url": "http://jcoglan.com/"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/faye/websocket-driver-node/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"websocket-extensions": ">=0.1.1"
|
||||
},
|
||||
"description": "WebSocket protocol handler with pluggable I/O",
|
||||
"devDependencies": {
|
||||
"jstest": "",
|
||||
"permessage-deflate": ""
|
||||
},
|
||||
"directories": {},
|
||||
"dist": {
|
||||
"shasum": "5cb2556ceb85f4373c6d8238aa691c8454e13a36",
|
||||
"tarball": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.6.5.tgz"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6.0"
|
||||
},
|
||||
"gitHead": "c4494ff88ac196f726bbb77a301c2177124b199e",
|
||||
"homepage": "https://github.com/faye/websocket-driver-node",
|
||||
"keywords": [
|
||||
"websocket"
|
||||
],
|
||||
"license": "MIT",
|
||||
"main": "./lib/websocket/driver",
|
||||
"maintainers": [
|
||||
{
|
||||
"email": "jcoglan@gmail.com",
|
||||
"name": "jcoglan"
|
||||
}
|
||||
],
|
||||
"name": "websocket-driver",
|
||||
"optionalDependencies": {},
|
||||
"readme": "ERROR: No README data found!",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/faye/websocket-driver-node.git"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "jstest spec/runner.js"
|
||||
},
|
||||
"version": "0.6.5"
|
||||
}
|
Loading…
Reference in New Issue