diff --git a/.eslintrc b/.eslintrc
index 6e125953..e16dcc44 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -14,6 +14,7 @@
"comma-dangle": 0,
"indent": [2, 2, {"SwitchCase": 1}],
"react/prop-types": 0,
- "jsx-quotes": [2, "prefer-single"]
+ "jsx-quotes": [2, "prefer-single"],
+ "no-param-reassign": ["error", { "props": false }]
}
}
diff --git a/css/components/_home.css b/css/components/_home.css
index 86b009e6..0256ebf4 100644
--- a/css/components/_home.css
+++ b/css/components/_home.css
@@ -184,3 +184,7 @@ input {
.ReactTags__selected .ReactTags__remove {
margin-left: 3px;
}
+
+.Select.is-open {
+ z-index: 100;
+}
diff --git a/index.html b/index.html
index 12fd8eb3..760c0258 100644
--- a/index.html
+++ b/index.html
@@ -5,7 +5,7 @@
-
+
diff --git a/js/app.js b/js/app.js
index ed9ab3c5..0481d933 100644
--- a/js/app.js
+++ b/js/app.js
@@ -13,11 +13,14 @@
// import 'file-loader?name=[name].[ext]!../.htaccess';
// Check for ServiceWorker support before trying to install it
-// if (process.env.NODE_ENV === 'production') {
-// if ('serviceWorker' in navigator) {
+// if (process.env.NODE_ENV === 'development') {
+// if ('serviceWorker' in navigator && 'PushManager' in window) {
// navigator.serviceWorker.register('/serviceworker.js')
// .then(
-// registration => console.log('ServiceWorker registration successful with scope: ', registration.scope),
+// registration => {
+// console.log('ServiceWorker registration successful with scope: ', registration.scope);
+// window.swRegistration = registration;
+// },
// err => console.log('ServiceWorker registration failed: ', err)
// ).catch(err => {
// // Registration failed
diff --git a/js/components/Email/EmailPanel/EmailPanel.jsx b/js/components/Email/EmailPanel/EmailPanel.jsx
index 127592e6..296c66ba 100644
--- a/js/components/Email/EmailPanel/EmailPanel.jsx
+++ b/js/components/Email/EmailPanel/EmailPanel.jsx
@@ -20,7 +20,6 @@ import isJSON from 'validator/lib/isJSON';
import Select from 'react-select';
-import VirtualizedSelect from 'react-virtualized-select';
import ReactTooltip from 'react-tooltip'
import RaisedButton from 'material-ui/RaisedButton';
import FlatButton from 'material-ui/FlatButton';
@@ -43,7 +42,6 @@ import PauseOverlay from './PauseOverlay.jsx';
import 'react-select/dist/react-select.css';
import 'react-virtualized/styles.css';
-import 'react-virtualized-select/styles.css';
import './react-select-hack.css';
import 'node_modules/alertifyjs/build/css/alertify.min.css';
import './ReactTagsStyle.css';
@@ -127,6 +125,7 @@ class EmailPanel extends Component {
this.onClearClick = this._onClearClick.bind(this);
this.checkEmailDupes = this._checkEmailDupes.bind(this);
this.changeEmailSignature = this._changeEmailSignature.bind(this);
+ this.onSendTestEmail = this.onSendTestEmail.bind(this);
// cleanups
this.onEmailSendClick = _ => this.checkEmailDupes().then(this.onPreviewEmailsClick);
@@ -226,6 +225,10 @@ class EmailPanel extends Component {
this.setState({bodyEditorState: templateJSON.data});
this.props.saveEditorState(templateJSON.data);
this.setState({subjectHtml});
+ if (templateJSON.date) {
+ window.Intercom('trackEvent', 'use_prev_email_template', {date: templateJSON.date});
+ mixpanel.track('use_prev_email_template', {date: templateJSON.date});
+ }
} else {
this.props.setBodyHtml(bodyHtml);
this.setState({bodyHtml, subjectHtml});
@@ -338,7 +341,6 @@ class EmailPanel extends Component {
else invalidEmailContacts.push(contact);
});
const {contactEmails, emptyFields} = this.getGeneratedHtmlEmails(validEmailContacts, subject, body);
- console.log(emptyFields);
Promise.resolve()
.then(_ =>
@@ -405,7 +407,47 @@ class EmailPanel extends Component {
.catch(_ => {
console.log('CANCELLED');
});
+ }
+ onSendTestEmail() {
+ // const {subject, body} = this.state;
+ // const email = this.props.person.email;
+ // let newHtml = html;
+
+ // this.state.fieldsmap.map(fieldObj => {
+ // let value = '';
+ // const replaceValue = _getter(contact, fieldObj);
+ // if (replaceValue) value = replaceValue;
+ // const regexValue = new RegExp('\{' + fieldObj.name + '\}', 'g');
+ // // count num custom vars used
+ // const matches = newHtml.match(regexValue);
+ // if (matches !== null) {
+ // if (!value) emptyFields.push(fieldObj.name);
+ // matchCount[fieldObj.name] = matches.length;
+ // }
+ // newHtml = newHtml.replace(regexValue, value);
+ // if (expectedMatches !== null) expectedMatches = expectedMatches.filter(match => match !== `{${fieldObj.name}}`);
+ // });
+
+ // const bodyObj = replaceAll(body, selectedContacts[i], this.state.fieldsmap);
+ // const subjectObj = replaceAll(subject, selectedContacts[i], this.state.fieldsmap);
+ // let emailObj = {
+ // listid: this.props.listId,
+ // to: contact.email,
+ // subject: subjectObj.html,
+ // body: bodyObj.html,
+ // contactid: contact.id,
+ // templateid: this.state.currentTemplateId,
+ // cc: this.props.cc.map(item => item.text),
+ // bcc: this.props.bcc.map(item => item.text),
+ // fromemail: this.props.from,
+ // };
+ // if (this.props.scheduledtime !== null) {
+ // emailObj.sendat = this.props.scheduledtime;
+ // }
+ // if (subjectObj.numMatches > 0) {
+ // emailObj.baseSubject = subject;
+ // }
}
_onClearClick() {
@@ -473,7 +515,13 @@ class EmailPanel extends Component {
{props.isImageReceiving &&
}
-
+
+ {
+ /*
+
+
+ */
+ }
name || 'ignore_column');
this.props.onAddHeaders(order)
.then(_ => {
- if (!this.props.didInvalidate) setTimeout(_ => this.props.router.push(`/tables/${this.props.listId}?justCreated=true`), 5000);
+ if (!this.props.didInvalidate) {
+ this.setState({isLoading: true});
+ setTimeout(_ => this.props.router.push(`/tables/${this.props.listId}?justCreated=true`), 2000);
+ }
});
}
@@ -180,12 +189,25 @@ class HeaderNaming extends Component {
_ => {});
}
+ onListPresetSelect(list) {
+ if (!list) {
+ this.setState({selected: undefined});
+ return;
+ }
+ const fieldsmap = generateTableFieldsmap(list)
+ .filter(field => field.customfield && !field.readonly && !this.state.options.some(option => option.value === field.value))
+ .map(field => ({value: field.value, label: field.name, selected: false}));
+ const options = [...this.state.options, ...fieldsmap];
+ this.setState({options, selected: list}, _ => this._headernames.recomputeGridSize());
+ }
+
render() {
const props = this.props;
const state = this.state;
return (
- {props.isReceiving &&
LOADING ... }
+ {props.isReceiving &&
+
LOADING ... }
{props.headers &&
@@ -195,6 +217,10 @@ class HeaderNaming extends Component {
Tabulae will start to aggregate feeds from each contact's social fields once its connected.
Upload Guide
+
+ Add Existing List Properties to Dropdown
+
+
}
@@ -239,8 +265,8 @@ class HeaderNaming extends Component {
/>}
label='Submit'
onClick={this.onSubmit} />
- {props.isProcessWaiting &&
-
Please be patient. This may take from a few seconds to a few minutes depending on file size. }
+ {(props.isProcessWaiting || state.isLoading) &&
+
Uploading... This may take from a few seconds to a few minutes depending on file size. }
{props.didInvalidate &&
Something went wrong while processing property headers.
@@ -272,17 +298,25 @@ const styles = {
backgroundColor: lightBlue50, padding: 20, margin: 10
},
headerContainer: {width: 750},
+ preset: {
+ container: {margin: 10},
+ label: {color: grey600}
+ },
+ waiting: {color: grey600}
};
const mapStateToProps = (state, props) => {
const listId = parseInt(props.params.listId, 10);
+ const lists = state.listReducer.lists.map(id => state.listReducer[id]);
+
return {
listId,
isProcessWaiting: state.fileReducer.isProcessWaiting,
isReceiving: state.headerReducer.isReceiving,
headers: state.headerReducer[listId],
didInvalidate: state.headerReducer.didInvalidate,
- error: state.headerReducer.error
+ error: state.headerReducer.error,
+ lists
};
};
diff --git a/js/components/ListTable/ColumnEditPanelHOC/Card.jsx b/js/components/ListTable/ColumnEditPanel/Card.jsx
similarity index 100%
rename from js/components/ListTable/ColumnEditPanelHOC/Card.jsx
rename to js/components/ListTable/ColumnEditPanel/Card.jsx
diff --git a/js/components/ListTable/ColumnEditPanel/ColumnEditPanel.jsx b/js/components/ListTable/ColumnEditPanel/ColumnEditPanel.jsx
new file mode 100644
index 00000000..cfac3181
--- /dev/null
+++ b/js/components/ListTable/ColumnEditPanel/ColumnEditPanel.jsx
@@ -0,0 +1,166 @@
+import React, { Component } from 'react';
+import { DragDropContext } from 'react-dnd';
+import {actions as listActions} from 'components/Lists';
+import HTML5Backend from 'react-dnd-html5-backend';
+import Container from './Container.jsx';
+import {connect} from 'react-redux';
+import Dialog from 'material-ui/Dialog';
+import FlatButton from 'material-ui/FlatButton';
+import {yellow50, grey600} from 'material-ui/styles/colors';
+import {generateTableFieldsmap, reformatFieldsmap} from 'components/ListTable/helpers';
+import Select from 'react-select';
+import alertify from 'alertifyjs';
+import 'react-select/dist/react-select.css';
+
+alertify.promisifyConfirm = (title, description) => new Promise((resolve, reject) => {
+ alertify.confirm(title, description, resolve, reject);
+});
+
+alertify.promisifyPrompt = (title, description, defaultValue) => new Promise((resolve, reject) => {
+ alertify.prompt(title, description, defaultValue, (e, value) => resolve(value), reject);
+});
+
+class ColumnEditPanel extends Component {
+ constructor(props) {
+ super(props);
+ const hiddenList = this.props.fieldsmap.filter(field => field.hidden && !field.tableOnly);
+ const showList = this.props.fieldsmap.filter(field => !field.hidden && !field.tableOnly);
+ this.state = {
+ hiddenList,
+ showList,
+ isUpdating: false,
+ dirty: false,
+ selected: this.props.list
+ };
+ this.onUpdateList = this.onUpdateList.bind(this);
+ this.onSubmit = this.onSubmit.bind(this);
+ this.onListPresetSelect = this.onListPresetSelect.bind(this);
+ }
+
+ componentWillMount() {
+ if (!this.props.lists || this.props.lists.length === 0) {
+ this.props.fetchLists();
+ }
+ }
+
+ onSubmit() {
+ const fieldsmap = reformatFieldsmap([...this.state.showList, ...this.state.hiddenList]);
+ const listBody = {
+ listId: this.props.listId,
+ name: this.props.list.name,
+ fieldsmap
+ };
+ this.setState({isUpdating: true});
+ this.props.patchList(listBody)
+ .then(_ => this.setState({isUpdating: false}, this.props.onRequestClose));
+ }
+
+ onUpdateList(list, containerType) {
+ this.setState({[containerType]: list, dirty: true});
+ }
+
+ onListPresetSelect(list) {
+ if (!list) list = this.props.list;
+ const fieldsmap = generateTableFieldsmap(list);
+ const hiddenList = fieldsmap.filter(field => field.hidden && !field.tableOnly);
+ const showList = fieldsmap.filter(field => !field.hidden && !field.tableOnly);
+ this.setState({showList, hiddenList, selected: list, dirty: true});
+ }
+
+ render() {
+ const state = this.state;
+ const actions = [
+ ,
+ ,
+ ];
+
+ // console.log(this.props.fieldsmap);
+
+ return (
+
+
+
+
+ Drag each card to reorder the order of you columns.
+ Drag column cards from Hidden Columns to Showing Columns to activate or de-activate default columns.
+ You can also create custom columns that you can use as template variable in emails.
+
+
+
+
+ There is a number of auto-generated columns that are activated when certain columns are not hidden. For example,
+ activating Instagram Likes and Instagram Comments also activates Likes-to-Comments ratio .
+
+
+
+
Apply Presets -
+
Use properties from a previously created list
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+const styles = {
+ instructionContainer: {margin: '20px 0'},
+ columnsContainer: {paddingTop: 20},
+ preset: {
+ label: {fontSize: '1.2em', color: grey600},
+ dropdown: {margin: 10}
+ },
+ panel: {
+ backgroundColor: yellow50,
+ margin: 10,
+ padding: 10
+ },
+};
+
+const mapStateToProps = (state, props) => {
+ const lists = state.listReducer.lists.map(id => state.listReducer[id]);
+ const listId = props.listId;
+ const list = state.listReducer[listId];
+
+ const rawFieldsmap = generateTableFieldsmap(list);
+ return {
+ fieldsmap: rawFieldsmap,
+ list: list,
+ lists,
+ };
+};
+
+const mapDispatchToProps = (dispatch, props) => {
+ return {
+ patchList: listObj => dispatch(listActions.patchList(listObj)),
+ fetchLists: _ => dispatch(listActions.fetchLists()),
+ };
+};
+
+export default connect(mapStateToProps, mapDispatchToProps)(DragDropContext(HTML5Backend)(ColumnEditPanel));
diff --git a/js/components/ListTable/ColumnEditPanelHOC/Container.jsx b/js/components/ListTable/ColumnEditPanel/Container.jsx
similarity index 95%
rename from js/components/ListTable/ColumnEditPanelHOC/Container.jsx
rename to js/components/ListTable/ColumnEditPanel/Container.jsx
index 181474f9..3af247c6 100644
--- a/js/components/ListTable/ColumnEditPanelHOC/Container.jsx
+++ b/js/components/ListTable/ColumnEditPanel/Container.jsx
@@ -24,6 +24,12 @@ class Container extends Component {
this.moveCard = this.moveCard.bind(this);
}
+ componentWillReceiveProps(nextProps) {
+ if (this.props.list !== nextProps.list) {
+ this.setState({cards: nextProps.list});
+ }
+ }
+
pushCard(card) {
const {updateList, containerType} = this.props;
const newCard = Object.assign({}, card, {hidden: containerType === 'hiddenList'});
@@ -129,8 +135,8 @@ const cardStyle = {
const cardTarget = {
hover(targetProps, monitor) {
- const sourceProps = monitor.getItem();
- // console.log(targetProps);
+ const sourceProps = monitor.getItem();
+ // console.log(targetProps);
},
drop(props, monitor, component) {
diff --git a/js/components/ListTable/ColumnEditPanelHOC/react_sortable_hoc.css b/js/components/ListTable/ColumnEditPanel/react_sortable_hoc.css
similarity index 100%
rename from js/components/ListTable/ColumnEditPanelHOC/react_sortable_hoc.css
rename to js/components/ListTable/ColumnEditPanel/react_sortable_hoc.css
diff --git a/js/components/ListTable/ColumnEditPanelHOC/ColumnEditPanelHOC.jsx b/js/components/ListTable/ColumnEditPanelHOC/ColumnEditPanelHOC.jsx
deleted file mode 100644
index 8e21c61d..00000000
--- a/js/components/ListTable/ColumnEditPanelHOC/ColumnEditPanelHOC.jsx
+++ /dev/null
@@ -1,148 +0,0 @@
-import React, { Component } from 'react';
-import { DragDropContext } from 'react-dnd';
-import {actions as listActions} from 'components/Lists';
-import HTML5Backend from 'react-dnd-html5-backend';
-import Container from './Container.jsx';
-import {connect} from 'react-redux';
-import Dialog from 'material-ui/Dialog';
-import FlatButton from 'material-ui/FlatButton';
-import RaisedButton from 'material-ui/RaisedButton';
-import {yellow50} from 'material-ui/styles/colors';
-
-import {
- generateTableFieldsmap,
- measureSpanSize,
- exportOperations,
- isNumber,
- _getter,
- reformatFieldsmap
-} from 'components/ListTable/helpers';
-import alertify from 'alertifyjs';
-
-alertify.promisifyConfirm = (title, description) => new Promise((resolve, reject) => {
- alertify.confirm(title, description, resolve, reject);
-});
-
-alertify.promisifyPrompt = (title, description, defaultValue) => new Promise((resolve, reject) => {
- alertify.prompt(title, description, defaultValue, (e, value) => resolve(value), reject);
-});
-
-class ColumnEditPanelHOC extends Component {
- constructor(props) {
- super(props);
- const hiddenList = this.props.fieldsmap.filter(field => field.hidden && !field.tableOnly);
- const showList = this.props.fieldsmap.filter(field => !field.hidden && !field.tableOnly);
- this.state = {
- hiddenList,
- showList,
- open: false,
- isUpdating: false,
- dirty: false,
- };
- this.updateList = this.updateList.bind(this);
- this.onSubmit = this.onSubmit.bind(this);
- }
-
- updateList(list, containerType) {
- this.setState({[containerType]: list, dirty: true});
- }
-
- onSubmit() {
- const fieldsmap = reformatFieldsmap([...this.state.showList, ...this.state.hiddenList]);
- const listBody = {
- listId: this.props.listId,
- name: this.props.list.name,
- fieldsmap
- };
- this.setState({isUpdating: true});
- this.props.patchList(listBody)
- .then(_ => this.setState({isUpdating: false, open: false}))
- }
-
- render() {
- const state = this.state;
- const actions = [
- this.setState({open: false})}
- />,
- ,
- ];
-
- // console.log(this.props.fieldsmap);
-
- return (
-
-
this.setState({open: false})}>
-
-
- Drag each card to reorder the order of you columns. Drag column cards from Hidden Columns to Showing Columns to activate or de-activate default columns. You can also create custom columns that you can use as template variable in emails.
-
-
-
-
-
- There is a number of auto-generated columns that are activated when certain columns are not hidden. For example,
- activating Instagram Likes and Instagram Comments also activates Likes-to-Comments ratio .
-
-
-
-
-
-
-
- {this.props.children({onRequestOpen: _ => this.setState({open: true})})}
-
- );
- }
-}
-
-const style = {
- // display: 'flex',
- // justifyContent: 'space-around',
- paddingTop: '20px'
-};
-
-const mapStateToProps = (state, props) => {
- const listId = props.listId;
- const list = state.listReducer[listId];
-
- const rawFieldsmap = generateTableFieldsmap(list);
- return {
- fieldsmap: rawFieldsmap,
- list: list
- }
-};
-
-const mapDispatchToProps = (dispatch, props) => {
- return {
- patchList: listObj => dispatch(listActions.patchList(listObj)),
- };
-};
-
-export default connect(mapStateToProps, mapDispatchToProps)(DragDropContext(HTML5Backend)(ColumnEditPanelHOC));
diff --git a/js/components/ListTable/CopyToHOC/CopyToHOC.jsx b/js/components/ListTable/CopyToHOC/CopyToHOC.jsx
index dd27a35a..66d0c145 100644
--- a/js/components/ListTable/CopyToHOC/CopyToHOC.jsx
+++ b/js/components/ListTable/CopyToHOC/CopyToHOC.jsx
@@ -93,9 +93,11 @@ class CopyToHOC extends Component {
{props.selected.length === 0 &&
none selected }
{props.selectedContacts &&
- {props.selectedContacts
- .map(contact => contact.firstname || contact.lastname || contact.email)
- .join(', ')} }
+ {
+ props.selectedContacts
+ .map(contact => contact.firstname || contact.lastname || contact.email || contact.id)
+ .join(', ')
+ } }
Select the List(s) to Copy these selected contacts to:
@@ -181,6 +183,7 @@ const styles = {
const mapStateToProps = (state, props) => {
const lists = state.listReducer.lists.map(id => state.listReducer[id]);
+
return {
lists,
list: state.listReducer[props.listId],
diff --git a/js/components/ListTable/EmptyListStatement.jsx b/js/components/ListTable/EmptyListStatement.jsx
index 7ff5e891..5312f406 100644
--- a/js/components/ListTable/EmptyListStatement.jsx
+++ b/js/components/ListTable/EmptyListStatement.jsx
@@ -3,10 +3,11 @@ import React from 'react';
const EmptyListStatement = ({className, style}) => (
-
You haven't added any contacts. You will see a master sheet of them here after you added some.
+
You haven't added any contact. You will see a master sheet of them here after you added some.
"Add Contact" icon on top to add ONE contact
Go back to Home and "Upload from Existing" Excel sheet
+ Want to use same columns as an another list? Use "Apply Presets" by clicking on icon
);
diff --git a/js/components/ListTable/ListTable.jsx b/js/components/ListTable/ListTable.jsx
index 6d79ba50..003513b8 100644
--- a/js/components/ListTable/ListTable.jsx
+++ b/js/components/ListTable/ListTable.jsx
@@ -36,7 +36,7 @@ import Drawer from 'material-ui/Drawer';
import {ControlledInput} from '../ToggleableEditInput';
import Waiting from '../Waiting';
import CopyToHOC from './CopyToHOC';
-import ColumnEditPanelHOC from 'components/ListTable/ColumnEditPanelHOC/ColumnEditPanelHOC.jsx';
+import ColumnEditPanel from 'components/ListTable/ColumnEditPanel/ColumnEditPanel.jsx';
import AddContactHOC from './AddContactHOC.jsx';
import AddTagDialogHOC from './AddTagDialogHOC.jsx';
import EditMultipleContactsHOC from './EditMultipleContactsHOC.jsx';
@@ -98,9 +98,10 @@ class ListTable extends Component {
scrollToRow: undefined,
currentSearchIndex: 0,
isDeleting: false,
- showEditPanel: false,
+ showContactEditPanel: false,
currentEditContactId: undefined,
showEmailPanel: true,
+ showColumnEditPanel: false,
};
// store outside of state to update synchronously for PanelOverlay
@@ -487,7 +488,7 @@ class ListTable extends Component {
);
contentBody2 = !this.props.listData.readonly &&
this.setState({currentEditContactId: rowData.id, showEditPanel: true})}
+ onClick={_ => this.setState({currentEditContactId: rowData.id, showContactEditPanel: true})}
className='fa fa-edit pointer'
style={styles.profileIcon}
color={blue300}
@@ -704,8 +705,8 @@ class ListTable extends Component {
this.setState({showEditPanel: false})}
+ open={state.showContactEditPanel}
+ onClose={_ => this.setState({showContactEditPanel: false})}
/>
{this.showProfileTooltip &&
)}
-
- {({onRequestOpen}) => (
- )}
-
+ this.setState({showColumnEditPanel: true})}
+ />
+ this.setState({showColumnEditPanel: false})} open={state.showColumnEditPanel} listId={props.listId} />
{({onRequestOpen}) => (
{
- const res = normalize(response, {
- data: arrayOf(listSchema),
- });
+ const res = normalize(response, {data: arrayOf(listSchema)});
const newOffset = response.data.length < PAGE_LIMIT ? null : OFFSET + PAGE_LIMIT;
return dispatch(receiveLists(res.entities.lists, res.result.data, newOffset));
})
@@ -129,9 +127,7 @@ export function fetchPublicLists() {
dispatch(requestLists());
return api.get(`/lists/public?limit=${PAGE_LIMIT}&offset=${OFFSET}`)
.then(response => {
- const res = normalize(response, {
- data: arrayOf(listSchema),
- });
+ const res = normalize(response, {data: arrayOf(listSchema)});
const newOffset = response.data.length < PAGE_LIMIT ? null : OFFSET + PAGE_LIMIT;
return dispatch({
type: listConstant.RECEIVE_MULTIPLE,
@@ -175,9 +171,7 @@ export function fetchTagLists(tagQuery) {
dispatch(requestLists());
return api.get(`/lists?q=tag:${tagQuery}&limit=${PAGE_LIMIT}&offset=${OFFSET}`)
.then(response => {
- const res = normalize(response, {
- data: arrayOf(listSchema),
- });
+ const res = normalize(response, {data: arrayOf(listSchema)});
const newOffset = response.data.length < PAGE_LIMIT ? null : OFFSET + PAGE_LIMIT;
return dispatch({
type: listConstant.RECEIVE_MULTIPLE,
@@ -200,9 +194,7 @@ export function fetchArchivedLists() {
dispatch(requestLists());
return api.get(`/lists/archived?limit=${PAGE_LIMIT}&offset=${OFFSET}&order=-Created`)
.then(response => {
- const res = normalize(response, {
- data: arrayOf(listSchema),
- });
+ const res = normalize(response, {data: arrayOf(listSchema)});
const newOffset = response.data.length < PAGE_LIMIT ? null : OFFSET + PAGE_LIMIT;
return dispatch({
type: listConstant.RECEIVE_MULTIPLE,
diff --git a/js/components/UserProfile/BasicSettings.jsx b/js/components/UserProfile/BasicSettings.jsx
index 8cb3ac8a..b5e4280f 100644
--- a/js/components/UserProfile/BasicSettings.jsx
+++ b/js/components/UserProfile/BasicSettings.jsx
@@ -4,6 +4,7 @@ import {ToggleableEditInputHOC, ToggleableEditInput} from '../ToggleableEditInpu
import {fromJS, is} from 'immutable';
import {grey500} from 'material-ui/styles/colors';
import RaisedButton from 'material-ui/RaisedButton';
+import Toggle from 'material-ui/Toggle';
import {actions as loginActions} from 'components/Login';
@@ -43,9 +44,26 @@ class BasicSettings extends Component {
this.state = {
immuperson: fromJS(this.props.person),
newPerson: fromJS(this.props.person),
+ notifySubscribed: false
};
+ // this.onToggle = this.onToggle.bind(this);
+ // this.onSubscribe = this.onSubscribe.bind(this);
+ // this.onUnsubscribe = this.onUnsubscribe.bind(this);
}
+ // componentWillMount() {
+ // navigator.serviceWorker.ready.then(swRegistration => {
+ // swRegistration.pushManager.getSubscription()
+ // .then(subscription => {
+ // const isSubscribed = !(subscription === null);
+ // console.log('subscription');
+ // console.log(isSubscribed);
+ // this.setState({notifySubscribed: isSubscribed});
+ // });
+ // window.swRegistration = swRegistration;
+ // });
+ // }
+
componentWillUnmount() {
if (!is(this.state.immuperson, this.state.newPerson)) {
const newPerson = this.state.newPerson;
@@ -59,6 +77,38 @@ class BasicSettings extends Component {
}
}
+ // onToggle(e, isToggled) {
+ // if (isToggled) this.onSubscribe();
+ // else this.onUnsubscribe();
+ // }
+
+ // onSubscribe() {
+ // window.swRegistration.pushManager
+ // .subscribe({userVisibleOnly: true})
+ // .then(subscription => {
+ // console.log(subscription);
+ // this.setState({notifySubscribed: true});
+ // })
+ // .catch(e => {
+ // console.log('Push Notify subscription denied by user');
+ // });
+ // }
+
+ // onUnsubscribe() {
+ // window.swRegistration.pushManager.getSubscription()
+ // .then(subscription => {
+ // if (!subscription) {
+ // this.setState({notifySubscribed: false});
+ // }
+ // subscription.unsubscribe()
+ // .then(_ => this.setState({notifySubscribed: false}));
+ // })
+ // .then(e => {
+ // console.log('failed to subscribe');
+ // });
+ // }
+
+
render() {
const {person} = this.props;
const state = this.state;
@@ -105,6 +155,16 @@ class BasicSettings extends Component {
/>}
+ {/*
+
+
+ Browser Notifications
+
+
+
+
+
+ */}
);
diff --git a/manifest.json b/manifest.json
index 6891b967..dfd0284f 100644
--- a/manifest.json
+++ b/manifest.json
@@ -30,5 +30,5 @@
"start_url": "index.html",
"display": "standalone",
"orientation": "portrait",
- "background_color": "#FFFFFF"
+ "background_color": "#FFFFFF",
}
\ No newline at end of file
diff --git a/package.json b/package.json
index 1cc85397..ec3f6b78 100644
--- a/package.json
+++ b/package.json
@@ -6,13 +6,14 @@
"alertifyjs": "^1.10.0",
"axios": "^0.9.1",
"classnames": "^2.2.5",
- "draft-convert": "^1.4.3",
+ "draft-convert": "^1.4.7",
"draft-js": "^0.10.0",
"es6-promise": "^4.0.5",
"fontfaceobserver": "^1.5.1",
"fuse.js": "^2.6.2",
"fuzzy": "^0.1.1",
"hopscotch": "^0.2.6",
+ "ifvisible.js": "^1.0.6",
"immutability-helper": "^2.2.2",
"immutable": "^3.8.1",
"install": "^0.8.1",
@@ -25,7 +26,6 @@
"moment": "^2.14.1",
"moment-timezone": "^0.5.9",
"normalizr": "^2.2.1",
- "npm": "^3.10.6",
"numbro": "^1.9.3",
"object-assign": "^4.1.0",
"pikaday": "^1.4.0",
@@ -70,8 +70,7 @@
"regression": "^1.2.1",
"sanitize-html": "^1.14.1",
"tlds": "^1.157.0",
- "validator": "^5.5.0",
- "zeroclipboard": "^2.2.0"
+ "validator": "^5.5.0"
},
"devDependencies": {
"appcache-webpack-plugin": "^1.2.0",
diff --git a/serviceworker.js b/serviceworker.js
index a811bbee..9c86de4f 100644
--- a/serviceworker.js
+++ b/serviceworker.js
@@ -1,61 +1,100 @@
-var CACHE_NAME = 'react-boilerplate-cache-v1';
-// The files we want to cache
-var urlsToCache = [
- // '/',
- '/css/main.css'
-// '/js/bundle.js'
-];
-
-// Set the callback for the install step
-self.addEventListener('install', function(event) {
- // Perform install steps
+self.addEventListener('push', function(event) {
+ console.log('Received a push message', event);
+
+ // var title = 'Yay a message.';
+ // var body = 'We have received a push message.';
+ // var tag = 'simple-push-demo-notification-tag';
+
// event.waitUntil(
- caches.open(CACHE_NAME)
- .then(function(cache) {
- console.log('Opened cache');
- return cache.addAll(urlsToCache);
- });
-});
+ // self.registration.showNotification(title, {
+ // body: body,
+ // tag: tag
+ // })
+ // );
+ function log(argument) {
+ console.log(argument);
+ }
+
+ function notification(args) {
+ var notifications = JSON.parse(args.data);
+ for (var i = notifications.length - 1; i >= 0; i--) {
+ self.registration.showNotification('Tabulae Notification', {
+ body: notifications[i].message,
+ });
+ }
+ }
-// Set the callback when the files get fetched
-self.addEventListener('fetch', function(event) {
- event.respondWith(
- caches.match(event.request)
- .then(function(response) {
- // Cached files available, return those
- if (response) {
- return response;
- }
-
- // IMPORTANT: Clone the request. A request is a stream and
- // can only be consumed once. Since we are consuming this
- // once by cache and once by the browser for fetch, we need
- // to clone the response
- var fetchRequest = event.request.clone();
-
- // Start request again since there are no files in the cache
- return fetch(fetchRequest).then(function(response) {
- // If response is invalid, throw error
- if (!response || response.status !== 200 || response.type !== 'basic') {
- return response;
- }
-
- // IMPORTANT: Clone the response. A response is a stream
- // and because we want the browser to consume the response
- // as well as the cache consuming the response, we need
- // to clone it so we have 2 stream.
- var responseToCache = response.clone();
-
- // Otherwise cache the downloaded files
- caches.open(CACHE_NAME)
- .then(function(cache) {
- cache.put(event.request, responseToCache);
- });
-
- // And return the network response
- return response;
- }
- );
- })
- );
+ event.waitUntil(
+ fetch('/users/me/token')
+ .then(response => {
+ const channel = new goog.appengine.Channel(response.token);
+ const socket = channel.open();
+ socket.onopen = log;
+ socket.onmessage = args => notification(args);
+ socket.onerror = log;
+ socket.onclose = log;
+ }));
});
+
+
+// var CACHE_NAME = 'react-boilerplate-cache-v1';
+// // The files we want to cache
+// var urlsToCache = [
+// // '/',
+// '/css/main.css'
+// // '/js/bundle.js'
+// ];
+
+// // Set the callback for the install step
+// self.addEventListener('install', function(event) {
+// // Perform install steps
+// // event.waitUntil(
+// caches.open(CACHE_NAME)
+// .then(function(cache) {
+// console.log('Opened cache');
+// return cache.addAll(urlsToCache);
+// });
+// });
+
+// // Set the callback when the files get fetched
+// self.addEventListener('fetch', function(event) {
+// event.respondWith(
+// caches.match(event.request)
+// .then(function(response) {
+// // Cached files available, return those
+// if (response) {
+// return response;
+// }
+
+// // IMPORTANT: Clone the request. A request is a stream and
+// // can only be consumed once. Since we are consuming this
+// // once by cache and once by the browser for fetch, we need
+// // to clone the response
+// var fetchRequest = event.request.clone();
+
+// // Start request again since there are no files in the cache
+// return fetch(fetchRequest).then(function(response) {
+// // If response is invalid, throw error
+// if (!response || response.status !== 200 || response.type !== 'basic') {
+// return response;
+// }
+
+// // IMPORTANT: Clone the response. A response is a stream
+// // and because we want the browser to consume the response
+// // as well as the cache consuming the response, we need
+// // to clone it so we have 2 stream.
+// var responseToCache = response.clone();
+
+// // Otherwise cache the downloaded files
+// caches.open(CACHE_NAME)
+// .then(function(cache) {
+// cache.put(event.request, responseToCache);
+// });
+
+// // And return the network response
+// return response;
+// }
+// );
+// })
+// );
+// });