Merged Import and Export branches, implemented Giulio's remarks on Import feature

This commit is contained in:
Dario Chiappetta
2015-05-21 14:32:51 +02:00
parent d1d5fae5de
commit 0126873868
43 changed files with 4494 additions and 1008 deletions

View File

@@ -0,0 +1,110 @@
/*
Copyright 2008-2015 Clipperz Srl
This file is part of Clipperz, the online password manager.
For further information about its features and functionalities please
refer to http://www.clipperz.com.
* Clipperz is free software: you can redistribute it and/or modify it
under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
* Clipperz is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU Affero General Public License for more details.
* You should have received a copy of the GNU Affero General Public
License along with Clipperz. If not, see http://www.gnu.org/licenses/.
*/
"use strict";
Clipperz.Base.module('Clipperz.PM.UI.Components.ExtraFeatures');
Clipperz.PM.UI.Components.ExtraFeatures.DataExportClass = React.createClass({
propTypes: {
// featureSet: React.PropTypes.oneOf(['FULL', 'EXPIRED', 'TRIAL']).isRequired,
// 'level': React.PropTypes.oneOf(['hide', 'info', 'warning', 'error']).isRequired
},
/*
jsonExport: function () {
MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'export', 'json');
},
htmlExport: function () {
MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'export', 'html');
},
*/
isFeatureEnabled: function (aValue) {
return (this.props['features'].indexOf(aValue) > -1);
},
handleDownloadOfflineCopyLink: function (anEvent) {
if (this.isFeatureEnabled('OFFLINE_COPY')) {
MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'downloadOfflineCopy');
}
},
handleExportLink: function () {
MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'downloadExport');
},
//=========================================================================
render: function () {
return React.DOM.div({className:'extraFeature devicePIN'}, [
React.DOM.h1({}, "Export"),
React.DOM.div({'className': 'content'}, [
React.DOM.ul({}, [
React.DOM.li({}, [
React.DOM.h3({}, "Offline copy"),
React.DOM.div({'className':'description'}, [
React.DOM.p({}, "Download a read-only portable version of Clipperz. Very convenient when no Internet connection is available."),
React.DOM.p({}, "An offline copy is just a single HTML file that contains both the whole Clipperz web application and your encrypted data."),
React.DOM.p({}, "It is as secure as the hosted Clipperz service since they both share the same code and security architecture.")
]),
React.DOM.a({'className':'button', 'onClick':this.handleDownloadOfflineCopyLink}, "download offline copy")
]),
React.DOM.li({}, [
React.DOM.h3({}, "HTML + JSON"),
React.DOM.div({'className':'description'}, [
React.DOM.p({}, "Download a printer-friendly HTML file that lists the content of all your cards."),
React.DOM.p({}, "This same file also contains all your data in JSON format."),
React.DOM.p({}, "Beware: all data are unencrypted! Therefore make sure to properly store and manage this file.")
]),
React.DOM.a({'className':'button', 'onClick':this.handleExportLink}, "download HTML+JSON")
]),
/*
React.DOM.li({}, [
React.DOM.h3({}, "Printing"),
React.DOM.div({'className':'description'}, [
React.DOM.p({}, "Click on the button below to open a new window displaying all your cards in a printable format."),
React.DOM.p({}, "If you are going to print for backup purposes, please consider the safer option provided by the “offline copy”.")
]),
React.DOM.a({'className':'button', 'onClick':this.htmlExport}, "HTML")
]),
React.DOM.li({}, [
React.DOM.h3({}, "Exporting to JSON"),
React.DOM.div({'className':'description'}, [
React.DOM.p({}, "JSON enables a “lossless” export of your cards. All the information will be preserved, including direct login configurations."),
React.DOM.p({}, "This custom format its quite convenient if you need to move some of all of your cards to a different Clipperz account. Or if you want to restore a card that has been accidentally deleted."),
React.DOM.p({}, "Click on the button below to start the export process.")
]),
React.DOM.a({'className':'button', 'onClick':this.jsonExport}, "JSON"),
])
*/
])
])
]);
},
//=========================================================================
});
Clipperz.PM.UI.Components.ExtraFeatures.DataExport = React.createFactory(Clipperz.PM.UI.Components.ExtraFeatures.DataExportClass);

View File

@@ -0,0 +1,172 @@
/*
Copyright 2008-2015 Clipperz Srl
This file is part of Clipperz, the online password manager.
For further information about its features and functionalities please
refer to http://www.clipperz.com.
* Clipperz is free software: you can redistribute it and/or modify it
under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
* Clipperz is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU Affero General Public License for more details.
* You should have received a copy of the GNU Affero General Public
License along with Clipperz. If not, see http://www.gnu.org/licenses/.
*/
"use strict";
Clipperz.Base.module('Clipperz.PM.UI.Components.ExtraFeatures');
var _steps = ['Input', 'CsvColumns', 'CsvLabels', 'CsvTitles', 'CsvNotes', 'CsvHidden', 'Preview'];
var _stepNames = ['Input', 'Columns', 'Labels', 'Titles', 'Notes','Hidden','Preview'];
Clipperz.PM.UI.Components.ExtraFeatures.DataImportClass = React.createClass({
_steps: _steps,
_stepNames: _stepNames,
_relevantSteps: {
'csv': _steps,
'json': [_steps[0], _steps[6]]
},
getInitialState: function() {
return {
'currentStep': this._steps[0],
'importContext': new Clipperz.PM.UI.ImportContext(),
'nextStepCallback': null,
'error': null
};
},
//=========================================================================
getStepIndex: function(aStep) {
return this._steps.indexOf(aStep);
},
getStepAfter: function() {
return this._steps[this.getStepIndex(this.state.currentStep) + 1];
},
getStepBefore: function() {
return this._steps[this.getStepIndex(this.state.currentStep) - 1];
},
isStepRelevant: function(aStep, aFormat) {
if (!aFormat) {
return true
} else {
return (this._relevantSteps[aFormat].indexOf(aStep) >= 0);
}
},
//--------------------------------------------------------------------------
goToStep: function(aStep) {
this.setState({
'currentStep': aStep,
'nextStepCallback': null,
'error': null
});
},
handleNextStepOnClick: function() {
if (this.state.nextStepCallback) {
var newImportContext = this.state.nextStepCallback();
if (newImportContext) {
MochiKit.Base.update(this.state.importContext, newImportContext);
if (this.state.currentStep == 'Input' && this.state.importContext.format == 'json') {
this.goToStep('Preview');
} else if (this.state.currentStep == 'Preview') {
this.state.importContext.resetContext();
this.goToStep('Input');
} else {
this.goToStep(this.getStepAfter());
}
} else {
if (this.state.currentStep == "Input") {
this.setState({'error': "unrecognized input format."});
} else {
this.setState({'error': "unknown error."});
}
}
}
},
handleBackOnClick: function() {
if (this.state.importContext.format == 'json' && this.state.currentStep == 'Preview') {
delete this.state.importContext.format;
this.goToStep('Input');
} else if (this.state.currentStep != this._steps[0]) {
this.goToStep(this.getStepBefore());
}
},
setNextStepCallback: function(aFunction) {
this.setState({'nextStepCallback': aFunction});
},
getStepNavbarClass: function(aStep) {
var result;
if (aStep == this.state.currentStep) {
result = 'active';
} else if (this.state.importContext.format == 'json' && (aStep>=1&&aStep<=5) ) {
result = 'disabled';
} else {
result = 'inactive';
}
return result;
},
//=========================================================================
render: function () {
return React.DOM.div({className:'extraFeature dataImport'}, [
React.DOM.h1({}, "Import"),
React.DOM.div({'className': 'content'}, [
React.DOM.ul({'className': 'stepNavbar'},
MochiKit.Base.map(MochiKit.Base.bind(function(aStep){
var className;
if (this.isStepRelevant(aStep,this.state.importContext.format)) {
className = (aStep == this.state.currentStep) ? 'active' : 'inactive';
} else {
className = 'disabled';
}
return React.DOM.li({
'className': className
}, this._stepNames[this.getStepIndex(aStep)]);
}, this),this._steps)
),
new Clipperz.PM.UI.Components.ExtraFeatures.DataImport[this.state.currentStep]({
'importContext': this.state.importContext,
'setNextStepCallback': this.setNextStepCallback,
}),
React.DOM.a({
'className': 'button'+((this.state.currentStep == this._steps[0]) ? ' disabled' : ''),
'onClick': this.handleBackOnClick,
}, "Back"),
React.DOM.a({
'className': 'button'+((! this.state.nextStepCallback) ? ' disabled' : ''),
'onClick': this.handleNextStepOnClick,
}, "Next"),
(this.state.error) ? React.DOM.p({'className': 'error'}, "Error: " + this.state.error) : null
])
]);
},
//=========================================================================
});
Clipperz.PM.UI.Components.ExtraFeatures.DataImport = React.createFactory(Clipperz.PM.UI.Components.ExtraFeatures.DataImportClass);

View File

@@ -26,26 +26,44 @@ Clipperz.Base.module('Clipperz.PM.UI.Components.ExtraFeatures.DataImport');
Clipperz.PM.UI.Components.ExtraFeatures.DataImport.CsvColumnsClass = React.createClass({
toggleColumn: function(columnN) {
var newState;
getInitialState: function() {
return {
'selectedColumns': this.props.importContext.selectedColumns
};
},
newState = {'importData': this.props.importState.importData};
newState.importData.selectedColumns[columnN] = ! newState.importData.selectedColumns[columnN];
componentDidMount() {
this.props.setNextStepCallback(this.handleNextStep);
},
//-------------------------------------------------------------------------
handleNextStep: function() {
return this.state;
},
//=========================================================================
toggleColumn: function(columnN) {
var newSelectedColumns;
newSelectedColumns = this.state.selectedColumns;
newSelectedColumns[columnN] = ! newSelectedColumns[columnN];
this.props.setImportStateCallback(newState);
this.setState({'selectedColumns': newSelectedColumns});
},
render: function() {
//console.log(this.props.importContext);
var columnSelectors;
var rowCount;
var i;
columnSelectors = [];
for (i=0; i<this.props.importState.importData.nColumns; i++) {
columnSelectors.push( React.DOM.td({'key': 'csv-colsel-'+i}, React.DOM.input({
for (i=0; i<this.props.importContext.nColumns; i++) {
columnSelectors.push( React.DOM.td({'key': 'csv-colsel-' + i}, React.DOM.input({
'type': 'checkbox',
'checked': this.props.importState.importData.selectedColumns[i],
'checked': this.state.selectedColumns[i],
'onChange': MochiKit.Base.partial(this.toggleColumn,i)
}) ) );
}
@@ -53,16 +71,8 @@ Clipperz.PM.UI.Components.ExtraFeatures.DataImport.CsvColumnsClass = React.creat
rowCount = 0;
return React.DOM.div({},[
React.DOM.h2({},"Columns"),
Clipperz.PM.UI.Components.ExtraFeatures.DataImport.StepsNavigation({
'format': 'csv',
'stepId': 'csv-columns',
'prevStep': 'input',
'nextStep': 'csv-labels',
'goToStepCallback': this.props.goToStepCallback,
}),
React.DOM.p({}, "Select the columns you want to import."),
React.DOM.table({'style': {'background': 'white'}},[
React.DOM.table({'className': 'csvTable'},[
React.DOM.thead({}, React.DOM.tr({'className': 'columnSelectors', 'key': 'csv-colsel'}, columnSelectors)),
React.DOM.tbody({},
MochiKit.Base.map(function(row){
@@ -70,13 +80,13 @@ Clipperz.PM.UI.Components.ExtraFeatures.DataImport.CsvColumnsClass = React.creat
var result
cellCount = 0;
result = React.DOM.tr({'key': 'csv-row-'+(rowCount++)}, MochiKit.Base.map(function(cell) {
return React.DOM.td({'key': 'csv-cell-'+rowCount+'-'+(cellCount++)},cell);
result = React.DOM.tr({'key': 'csv-row-' + (rowCount++)}, MochiKit.Base.map(function(cell) {
return React.DOM.td({'key': 'csv-cell-' + rowCount + '-' + (cellCount++)},cell);
}, row));
rowCount++;
return result;
}, this.props.importState.importData.parsedCSV)
}, this.props.importContext.parsedCsv)
),
])
]);

View File

@@ -25,53 +25,118 @@ refer to http://www.clipperz.com.
Clipperz.Base.module('Clipperz.PM.UI.Components.ExtraFeatures.DataImport');
Clipperz.PM.UI.Components.ExtraFeatures.DataImport.CsvHiddenClass = React.createClass({
checkedCallback: function(columnN) {
return this.props.importState.importData.hiddenColumns[columnN];
getInitialState: function() {
return {
'hiddenColumns': this.props.importContext.hiddenColumns
};
},
componentDidMount() {
this.props.setNextStepCallback(this.handleNextStep);
},
//-------------------------------------------------------------------------
handleNextStep: function() {
//var importData = this.props.importState.importData;
//var json = this.props.csvToJsonCallback();
//this.props.setImportStateCallback({
// 'importData': importData,
// 'jsonToImport': json,
// 'recordsToImport': MochiKit.Base.map(function(r){return r._importId},json),
// 'currentStep': 'preview',
// 'previousStep': 'csv-hidden'
//})
MochiKit.Base.update(this.props.importContext, this.state);
return true;
},
//=========================================================================
onChangeCallback: function(columnN) {
var newState = {'importData': this.props.importState.importData};
var newHiddenColumns = this.state.hiddenColumns;
newState.importData.hiddenColumns[columnN] = ! newState.importData.hiddenColumns[columnN];
newHiddenColumns[columnN] = ! newHiddenColumns[columnN];
this.setState(newState);
},
disabledCallback: function(columnN) {
return (columnN == this.props.importState.importData.titlesColumn || columnN == this.props.importState.importData.notesColumn)
this.setState({'hiddenColumns': newHiddenColumns});
},
render: function() {
var importData = this.props.importState.importData;
var cellCount, rowCount;
var importContext = this.props.importContext;
cellCount = 0;
rowCount = 0;
return React.DOM.div({},[
React.DOM.h2({},"Hidden"),
Clipperz.PM.UI.Components.ExtraFeatures.DataImport.StepsNavigation({
'format': 'csv',
'stepId': 'csv-hidden'
}),
React.DOM.button({'onClick': MochiKit.Base.partial(this.props.goToStepCallback, 'csv-notes') }, "Back"),
React.DOM.span({}, " - "),
React.DOM.button({'onClick': MochiKit.Base.bind(function() {
var importData = this.props.importState.importData;
var json = this.props.csvToJsonCallback();
this.props.setImportStateCallback({
'importData': importData,
'jsonToImport': json,
'recordsToImport': MochiKit.Base.map(function(r){return r._importId},json),
'currentStep': 'preview',
'previousStep': 'csv-hidden'
});
}, this) }, "Preview"),
React.DOM.p({}, "Select the fields that should be hidden. (passwords, PINs, ...)"),
React.DOM.table({'style': {'background': 'white'}},[
React.DOM.table({'className': 'csvTable'},[
React.DOM.thead({},
this.props.csvRenderTheadInputCallback('hidden', 'checkbox', this.checkedCallback, this.onChangeCallback, this.disabledCallback, true)
React.DOM.tr({},
MochiKit.Base.map(MochiKit.Base.bind(function(cell) {
var result;
var thId = 'csv-notes-header-' + cellCount;
var inputId = 'csv-notes-input-' + cellCount;
if (! importContext.selectedColumns[cellCount]) {
result = null;
} else {
result = React.DOM.th({'key': thId}, [
React.DOM.label({'htmlFor': inputId}, importContext.getCsvLabels()[cellCount]),
React.DOM.input({
'type': 'checkbox',
'id': inputId,
'key': inputId,
'ref': inputId,
'checked': this.state.hiddenColumns[cellCount],
'onChange': MochiKit.Base.partial(this.onChangeCallback,cellCount),
'disabled': (cellCount == importContext.titlesColumn || cellCount == importContext.notesColumn)
})
]);
}
cellCount++;
return result;
}, this), importContext.parsedCsv[0])
)
),
React.DOM.tbody({},
this.props.csvRenderTbodyCallback()
MochiKit.Base.map(MochiKit.Base.bind(function(row){
var result;
cellCount = 0;
if (rowCount == 0 && importContext.firstRowAsLabels) {
result = null;
} else {
result = React.DOM.tr({'key': 'csv-row-' + (rowCount)}, MochiKit.Base.map( function(cell) {
var result;
if (importContext.selectedColumns[cellCount]) {
result = React.DOM.td({'key': 'csv-cell-' + rowCount + '-' + (cellCount)},cell);
} else{
result = null;
}
cellCount++;
return result;
}, row));
}
rowCount++;
return result;
},this), importContext.parsedCsv)
)
])

View File

@@ -26,84 +26,153 @@ Clipperz.Base.module('Clipperz.PM.UI.Components.ExtraFeatures.DataImport');
Clipperz.PM.UI.Components.ExtraFeatures.DataImport.CsvLabelsClass = React.createClass({
toggleFirstRow: function() {
var newState;
var cellCount;
getInitialState: function() {
return {
'firstRowAsLabels': this.props.importContext.firstRowAsLabels,
'columnLabels': this.props.importContext.columnLabels,
'columnLabelsFirstrow': this.props.importContext.columnLabelsFirstrow
};
},
componentDidMount() {
this.props.setNextStepCallback((this.isNextDisabled()) ? null : this.handleNextStep);
},
//-------------------------------------------------------------------------
newState = {'importData': this.props.importState.importData};
newState.importData.firstRowAsLabels = ! newState.importData.firstRowAsLabels;
cellCount = 0;
MochiKit.Base.map(function(cell){
newState.importData.columnLabelsFirstrow[cellCount++] = cell;
}, this.props.importState.importData.parsedCSV[0]);
this.props.setImportStateCallback(newState);
handleNextStep: function() {
return this.state;
},
updateNextStatus: function() {
this.props.setNextStepCallback((! this.isNextDisabled()) ? this.handleNextStep : null);
},
isNextDisabled: function() {
var result;
var importData = this.props.importState.importData;
var columnLabels = (importData.firstRowAsLabels) ? importData.columnLabelsFirstrow : importData.columnLabels;
var importContext = this.props.importContext;
var columnLabels = this.getLabels();
result = false;
for (i in columnLabels) {
result = result || (columnLabels[i] == '');
result = result || ((columnLabels[i] == '')&&(importContext.selectedColumns[i]));
}
return result;
},
valueCallback: function(columnN) {
var columnLabels = this.props.csvGetColumnLabelsCallback();
return columnLabels[columnN];
//=========================================================================
getLabels: function() {
return (this.state.firstRowAsLabels) ? this.state.columnLabelsFirstrow : this.state.columnLabels;
},
toggleFirstRow: function() {
var newState;
var cellCount;
newState = this.state;
newState.firstRowAsLabels = ! newState.firstRowAsLabels;
cellCount = 0;
MochiKit.Base.map(function(cell){
newState.columnLabelsFirstrow[cellCount++] = cell;
}, this.props.importContext.parsedCsv[0]);
this.updateNextStatus();
this.setState(newState);
},
onChangeCallback: function(columnN) {
var newState;
newState = {'importData': this.props.importState.importData};
if (this.props.importState.importData.firstRowAsLabels) {
newState.importData.columnLabelsFirstrow[columnN] = this.refs['csv-labels-input-'+columnN].getDOMNode().value;
newState = this.state;
if (newState.firstRowAsLabels) {
newState.columnLabelsFirstrow[columnN] = this.refs['csv-labels-input-' + columnN].getDOMNode().value;
} else {
newState.importData.columnLabels[columnN] = this.refs['csv-labels-input-'+columnN].getDOMNode().value;
newState.columnLabels[columnN] = this.refs['csv-labels-input-' + columnN].getDOMNode().value;
}
this.props.setImportStateCallback(newState);
this.updateNextStatus();
this.setState(newState);
},
render: function() {
//console.log("labels-render",this.props.importContext);
//return React.DOM.p({}, "labels")
var rowCount, cellCount;
var importContext = this.props.importContext;
var columnLabels = this.getLabels();
var importData = this.props.importState.importData;
rowCount = 0;
cellCount = 0;
return React.DOM.div({},[
React.DOM.h2({},"Labels"),
Clipperz.PM.UI.Components.ExtraFeatures.DataImport.StepsNavigation({
'format': 'csv',
'stepId': 'csv-labels',
'prevStep': 'csv-columns',
'nextStep': 'csv-titles',
'goToStepCallback': this.props.goToStepCallback,
'nextDisabled': this.isNextDisabled()
}),
React.DOM.p({}, "Set a label for each field in your data. If the first row of the CSV file contains field labels, tick off the checkbox below."),
React.DOM.input({
'id': 'csv-labels-firstrow',
'type': 'checkbox',
'checked': this.props.importState.importData.firstRowAsLabels,
'checked': this.state.firstRowAsLabels,
'onChange': this.toggleFirstRow
}),
React.DOM.label({'htmlFor':'csv-labels-firstrow'}, "Use the first row as labels"),
React.DOM.table({'style': {'background': 'white'}},[
React.DOM.table({'className': 'csvTable'},[
React.DOM.thead({},
this.props.csvRenderTheadInputCallback('labels', 'text', this.valueCallback, this.onChangeCallback, null, false)
React.DOM.tr({},
MochiKit.Base.map(MochiKit.Base.bind(function(cell) {
var result;
if (! importContext.selectedColumns[cellCount]) {
result = null;
} else {
result = React.DOM.th({'key': 'csv-labels-header-' + cellCount}, [
React.DOM.input({
'type': 'text',
'id': 'csv-labels-input-' + cellCount,
'key': 'csv-labels-input-' + cellCount,
'ref': 'csv-labels-input-' + cellCount,
'value': columnLabels[cellCount],
'onChange': MochiKit.Base.partial(this.onChangeCallback,cellCount)
})
]);
}
cellCount++;
return result;
}, this), this.props.importContext.parsedCsv[0])
)
),
React.DOM.tbody({},
this.props.csvRenderTbodyCallback()
MochiKit.Base.map(MochiKit.Base.bind(function(row){
var result;
cellCount = 0;
if (rowCount == 0 && this.state.firstRowAsLabels) {
result = null;
} else {
result = React.DOM.tr({'key': 'csv-row-' + (rowCount)}, MochiKit.Base.map( function(cell) {
var result;
if (importContext.selectedColumns[cellCount]) {
result = React.DOM.td({'key': 'csv-cell-' + rowCount + '-' + (cellCount)},cell);
} else{
result = null;
}
cellCount++;
return result;
}, row));
}
rowCount++;
return result;
},this), importContext.parsedCsv)
)
])

View File

@@ -26,50 +26,109 @@ Clipperz.Base.module('Clipperz.PM.UI.Components.ExtraFeatures.DataImport');
Clipperz.PM.UI.Components.ExtraFeatures.DataImport.CsvNotesClass = React.createClass({
checkedCallback: function(columnN) {
return columnN == this.props.importState.importData.notesColumn;
getInitialState: function() {
return {
'notesColumn': this.props.importContext.notesColumn
};
},
componentDidMount() {
this.props.setNextStepCallback(this.handleNextStep);
},
//-------------------------------------------------------------------------
handleNextStep: function() {
return this.state;
},
//=========================================================================
onChangeCallback: function(columnN) {
var newState = {'importData': this.props.importState.importData};
newState.importData.notesColumn = columnN;
this.setState(newState);
},
disabledCallback: function(columnN) {
return columnN == this.props.importState.importData.titlesColumn;
this.setState({'notesColumn': columnN});
},
render: function() {
var cellCount, rowCount;
var importContext = this.props.importContext;
cellCount = 0;
rowCount = 0;
return React.DOM.div({},[
React.DOM.h2({},"Notes"),
Clipperz.PM.UI.Components.ExtraFeatures.DataImport.StepsNavigation({
'format': 'csv',
'stepId': 'csv-notes',
'prevStep': 'csv-titles',
'nextStep': 'csv-hidden',
'goToStepCallback': this.props.goToStepCallback
}),
React.DOM.p({}, "Select the column that represents a \"notes\" field. (optional)"),
React.DOM.input({
'id': 'csv-notes-nonotes',
'type': 'radio',
'checked': ! this.props.importState.importData.notesColumn,
'checked': ! this.state.notesColumn,
'onChange': MochiKit.Base.partial(this.onChangeCallback, null)
}),
React.DOM.label({'htmlFor': 'csv-notes-nonotes'}, "\"notes\" field not present"),
React.DOM.table({'style': {'background': 'white'}},[
React.DOM.table({'className': 'csvTable'},[
React.DOM.thead({},
this.props.csvRenderTheadInputCallback('notes', 'radio', this.checkedCallback, this.onChangeCallback, this.disabledCallback, true)
React.DOM.tr({},
MochiKit.Base.map(MochiKit.Base.bind(function(cell) {
var result;
var thId = 'csv-notes-header-' + cellCount;
var inputId = 'csv-notes-input-' + cellCount;
if (! importContext.selectedColumns[cellCount]) {
result = null;
} else {
result = React.DOM.th({'key': thId}, [
React.DOM.label({'htmlFor': inputId}, importContext.getCsvLabels()[cellCount]),
React.DOM.input({
'type': 'radio',
'id': inputId,
'key': inputId,
'ref': inputId,
'checked': cellCount == this.state.notesColumn,
'onChange': MochiKit.Base.partial(this.onChangeCallback,cellCount),
'disabled': cellCount == importContext.titlesColumn
})
]);
}
cellCount++;
return result;
}, this), importContext.parsedCsv[0])
)
),
React.DOM.tbody({},
this.props.csvRenderTbodyCallback()
MochiKit.Base.map(MochiKit.Base.bind(function(row){
var result;
cellCount = 0;
if (rowCount == 0 && importContext.firstRowAsLabels) {
result = null;
} else {
result = React.DOM.tr({'key': 'csv-row-' + (rowCount)}, MochiKit.Base.map( function(cell) {
var result;
if (importContext.selectedColumns[cellCount]) {
result = React.DOM.td({'key': 'csv-cell-' + rowCount + '-' + (cellCount)},cell);
} else{
result = null;
}
cellCount++;
return result;
}, row));
}
rowCount++;
return result;
},this), importContext.parsedCsv)
)
])
]);
}

View File

@@ -26,45 +26,115 @@ Clipperz.Base.module('Clipperz.PM.UI.Components.ExtraFeatures.DataImport');
Clipperz.PM.UI.Components.ExtraFeatures.DataImport.CsvTitlesClass = React.createClass({
checkedCallback: function(columnN) {
return columnN == this.props.importState.importData.titlesColumn;
getInitialState: function() {
return {
'titlesColumn': this.props.importContext.titlesColumn,
'notesColumn': this.props.importContext.notesColumn
};
},
onChangeCallback: function(columnN) {
var newState = {'importData': this.props.importState.importData};
componentDidMount() {
this.props.setNextStepCallback((this.isNextDisabled()) ? null : this.handleNextStep);
},
if (this.props.importState.importData.notesColumn == columnN) {
newState.importData.notesColumn = null;
//-------------------------------------------------------------------------
handleNextStep: function() {
return this.state;
},
updateNextStatus: function() {
this.props.setNextStepCallback((! this.isNextDisabled()) ? this.handleNextStep : null);
},
isNextDisabled: function() {
return (this.state.titlesColumn != 0 && ! this.state.titlesColumn );
},
//=========================================================================
onChangeCallback: function(columnN) {
var newState = this.state;
if (newState.notesColumn == columnN) {
newState.notesColumn = null;
}
newState.importData.titlesColumn = columnN;
newState.titlesColumn = columnN;
this.props.setImportStateCallback(newState);
this.updateNextStatus();
this.setState(newState);
},
render: function() {
var rowCount, cellCount;
var importData = this.props.importState.importData;
var importContext = this.props.importContext;
var columnLabels = importContext.getCsvLabels();
rowCount = 0;
cellCount = 0;
return React.DOM.div({},[
React.DOM.h2({},"Titles"),
Clipperz.PM.UI.Components.ExtraFeatures.DataImport.StepsNavigation({
'format': 'csv',
'stepId': 'csv-titles',
'prevStep': 'csv-labels',
'nextStep': 'csv-notes',
'goToStepCallback': this.props.goToStepCallback,
'nextDisabled': (importData.titlesColumn != 0 && ! importData.titlesColumn )
}),
React.DOM.p({}, "Select the column that contains titles of the cards you are importing. (mandatory)"),
React.DOM.table({'style': {'background': 'white'}},[
React.DOM.table({'className': 'csvTable'},[
React.DOM.thead({},
this.props.csvRenderTheadInputCallback('titles', 'radio', this.checkedCallback, this.onChangeCallback, null, true)
React.DOM.tr({},
MochiKit.Base.map(MochiKit.Base.bind(function(cell) {
var result;
var thId = 'csv-titles-header-' + cellCount;
var inputId = 'csv-titles-input-' + cellCount;
if (! importContext.selectedColumns[cellCount]) {
result = null;
} else {
result = React.DOM.th({'key': thId}, [
React.DOM.label({'htmlFor': inputId}, columnLabels[cellCount]),
React.DOM.input({
'type': 'radio',
'id': inputId,
'key': inputId,
'ref': inputId,
'checked': cellCount == this.state.titlesColumn,
'onChange': MochiKit.Base.partial(this.onChangeCallback,cellCount)
})
]);
}
cellCount++;
return result;
}, this), this.props.importContext.parsedCsv[0])
)
),
React.DOM.tbody({},
this.props.csvRenderTbodyCallback()
MochiKit.Base.map(MochiKit.Base.bind(function(row){
var result;
cellCount = 0;
if (rowCount == 0 && importContext.firstRowAsLabels) {
result = null;
} else {
result = React.DOM.tr({'key': 'csv-row-'+(rowCount)}, MochiKit.Base.map( function(cell) {
var result;
if (importContext.selectedColumns[cellCount]) {
result = React.DOM.td({'key': 'csv-cell-' + rowCount + '-' + (cellCount)},cell);
} else{
result = null;
}
cellCount++;
return result;
}, row));
}
rowCount++;
return result;
},this), importContext.parsedCsv)
)
])

View File

@@ -25,6 +25,129 @@ refer to http://www.clipperz.com.
Clipperz.Base.module('Clipperz.PM.UI.Components.ExtraFeatures.DataImport');
Clipperz.PM.UI.Components.ExtraFeatures.DataImport.InputClass = React.createClass({
getInitialState: function() {
return {
'inputString': (this.props.importContext.inputString) ? this.props.importContext.inputString : null,
'format': (this.props.importContext.format) ? this.props.importContext.format : null,
//'parsedInput': (this.props.importContext.parsedInput) ? this.props.importContext.parsedInput : null,
};
},
componentDidMount: function() {
this.updateNextStatus(this.state.inputString);
},
//-------------------------------------------------------------------------
handleNextStep: function() {
var result;
var jsonData;
var parsedInput;
var inputString = this.refs['input-textarea'].getDOMNode().value.trim();
result = {'inputString': inputString};
parsedInput = this.parseJson(inputString);
if (parsedInput) {
MochiKit.Base.update(result,this.props.importContext.getInitialJsonContext(parsedInput));
} else {
parsedInput = this.parseCsv(inputString);
if (parsedInput) {
MochiKit.Base.update(result, this.props.importContext.getInitialCsvContext(parsedInput));
} else {
result = false;
}
}
return result;
},
updateNextStatus: function(newInputString) {
this.props.setNextStepCallback((newInputString) ? this.handleNextStep : null);
},
//=========================================================================
extractJsonFromClipperzExport: function(someHtml) {
var textarea;
var regexMatch;
var result;
var re = new RegExp('.*<textarea>(.*)<\/textarea>.*','g');
if (re.test(someHtml)) {
textarea = this.refs['input-textarea'].getDOMNode();
textarea.innerHTML = someHtml.replace(re, '$1');
result = textarea.innerHTML;
} else {
result = false;
}
return result;
},
addImportIds: function (someJson) {
var count;
for (count=0; count < someJson.length; count++) {
someJson[count]['_importId'] = count;
}
},
parseJson: function(aJsonString) {
var result;
var jsonData;
try {
jsonData = JSON.parse(aJsonString);
this.addImportIds(jsonData);
result = jsonData;
} catch(e) {
result = false;
}
return result;
},
parseCsv: function(aCsvString) {
var result;
var i;
var parsedCsv = Papa.parse(aCsvString);
if (parsedCsv.errors.length != 0) {
result = false;
} else {
result = this.csvFillEmptyCells(parsedCsv.data);
}
return result;
},
csvFillEmptyCells: function(table) {
var i,j;
var result = [];
var maxColumns = MochiKit.Iter.reduce(function(prev,next) {
return Math.max(prev,next)
}, MochiKit.Base.map(function(row) {return row.length;}, table) );
for (i=0; i<table.length; i++) {
result[i] = [];
for (j=0; j<maxColumns; j++) {
result[i][j] = (typeof(table[i][j]) != "undefined") ? table[i][j] : "";
}
}
return result;
},
//=========================================================================
handleUploadFiles: function(someFiles) {
var file;
var reader;
@@ -33,19 +156,24 @@ Clipperz.PM.UI.Components.ExtraFeatures.DataImport.InputClass = React.createClas
file = someFiles[0];
reader = new FileReader();
// TODO: check what happens with binary files
// Binary files are just thrown in the textarea as weird UTF-8 characters: should we do something about it?
reader.onloadend = MochiKit.Base.bind(function() {
var extractedJson = this.props.extractJsonFromClipperzExportCallback(reader.result);
var extractedJson = this.extractJsonFromClipperzExport(reader.result);
var newInputString;
if (extractedJson) {
this.props.setImportStateCallback({'importData': {'input': extractedJson}});
newInputString = extractedJson;
} else {
this.props.setImportStateCallback({'importData': {'input': reader.result}});
newInputString = reader.result;
}
this.setState({'inputString': newInputString});
this.updateNextStatus(newInputString);
},this,reader);
reader.readAsText(file);
} else {
// Should this be removed?
alert("Error: expecting a file as input.");
}
},
@@ -70,36 +198,17 @@ Clipperz.PM.UI.Components.ExtraFeatures.DataImport.InputClass = React.createClas
e.preventDefault();
},
handleSubmit: function(event) {
var jsonData;
var newState;
var inputString = this.refs['input-textarea'].getDOMNode().value.trim();
event.preventDefault();
if (newState = this.props.parseJsonCallback(inputString)) {
this.props.setImportStateCallback(newState);
} else if (newState = this.props.parseCsvCallback(inputString)) {
this.props.setImportStateCallback(newState);
} else {
alert("Unrecognized input format...");
}
},
handleTextareaChange: function() {
this.props.setImportStateCallback({'importData': {'input': this.refs['input-textarea'].getDOMNode().value}});
var newInputString = this.refs['input-textarea'].getDOMNode().value;
this.setState({'inputString': newInputString});
this.updateNextStatus(newInputString);
},
//=========================================================================
render: function() {
return React.DOM.div({},[
React.DOM.h2({},"Input"),
Clipperz.PM.UI.Components.ExtraFeatures.DataImport.StepsNavigation({
'format': 'json',
'stepId': 'input'
}),
React.DOM.form({'key':'form', 'className':'importForm', 'onSubmit': this.handleSubmit }, [
React.DOM.button({'key':'input-next', 'type':'submit', 'className':'button'}, "Next"),
React.DOM.form({'key':'form', 'className':'importForm' }, [
React.DOM.input({
'type': 'file',
'ref': 'upload-input',
@@ -108,15 +217,10 @@ Clipperz.PM.UI.Components.ExtraFeatures.DataImport.InputClass = React.createClas
'style': {'display': 'none'}
}),
React.DOM.div({
'style': { // TODO: replace with proper CSS
'width': '90%',
'textAlign': 'center',
'lineHeight': '3em',
'border': '3px dashed white'
},
'onDragOver': this.handleOnDragOver,
'onDrop': this.handleOnDrop,
'onClick': MochiKit.Base.bind(function() { this.refs['upload-input'].getDOMNode().click() }, this)
'onClick': MochiKit.Base.bind(function() { this.refs['upload-input'].getDOMNode().click() }, this),
'className': 'dropArea'
}, "Drag your Clipperz export file here or click select it manually."),
React.DOM.p({}, "or"),
React.DOM.div({'key':'fields'},[
@@ -125,7 +229,7 @@ Clipperz.PM.UI.Components.ExtraFeatures.DataImport.InputClass = React.createClas
'name':'input-textarea',
'ref':'input-textarea',
'placeholder':"Open the JSON file exported from Clipperz in a text editor. Then copy and paste its content here.",
'value': this.props.importState.importData.input,
'value': this.state.inputString,
'onChange': this.handleTextareaChange,
'onDragOver': this.handleOnDragOver,
'onDrop': this.handleOnDrop,

View File

@@ -26,28 +26,81 @@ Clipperz.Base.module('Clipperz.PM.UI.Components.ExtraFeatures.DataImport');
Clipperz.PM.UI.Components.ExtraFeatures.DataImport.PreviewClass = React.createClass({
// UNCOMMENT AFTER MERGE (uses methods in Record that were added in another branch)
// getTags: function (aTitle) {
// var result;
// var tagList;
//
// var tagObject = Clipperz.PM.DataModel.Record.extractTagsFromFullLabel(aTitle);
//
// tagList = MochiKit.Base.keys(tagObject);
// tagList = MochiKit.Base.filter(function(aTag) { return tagObject[aTag] }, tagList);
//
// if (tagList.length > 0) {
// result = React.DOM.ul({'className': 'tagList'},
// MochiKit.Base.map(function(aTag){
// return React.DOM.li({}, aTag);
// }, tagList)
// );
// } else {
// result = null;
// }
//
// return result;
// },
getInitialState: function() {
if (this.props.importContext.format == 'csv') {
return this.props.importContext.processCsv()
} else {
return {
'jsonToImport': this.props.importContext.jsonToImport,
'recordsToImport': this.props.importContext.recordsToImport,
}
}
},
componentDidMount() {
this.props.setNextStepCallback(this.handleImport);
},
//-------------------------------------------------------------------------
handleImport: function() {
MochiKit.Base.update(this.props.importContext, this.state);
var filteredImportData = MochiKit.Base.filter(
MochiKit.Base.bind(function(r) {
return this.isRecordToImport(r);
}, this),
this.state.jsonToImport
);
MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'importCards', filteredImportData);
return true;
},
//=========================================================================
toggleRecordToImport: function(record) {
var newRecordsToImport;
var recordPosition;
newRecordsToImport = this.state.recordsToImport;
recordPosition = newRecordsToImport.indexOf(record._importId);
if (recordPosition === -1) {
newRecordsToImport.push(record._importId);
} else {
newRecordsToImport.splice(recordPosition,1);
}
this.setState({'recordsToImport': newRecordsToImport});
},
isRecordToImport: function(record) {
return (this.state.recordsToImport.indexOf(record._importId)>=0) ? true : false;
},
getTags: function (aTitle) {
var result;
var tagList;
var tagObject = Clipperz.PM.DataModel.Record.extractTagsFromFullLabel(aTitle);
tagList = MochiKit.Base.keys(tagObject);
tagList = MochiKit.Base.filter(function(aTag) { return tagObject[aTag] }, tagList);
if (tagList.length > 0) {
result = React.DOM.ul({'className': 'tagList'},
MochiKit.Base.map(function(aTag){
return React.DOM.li({}, aTag);
}, tagList)
);
} else {
result = null;
}
return result;
},
renderCardFields: function(someFields) {
return MochiKit.Base.map(function(key) {
@@ -65,13 +118,11 @@ Clipperz.PM.UI.Components.ExtraFeatures.DataImport.PreviewClass = React.createCl
return React.DOM.li({'className': 'card'}, [
React.DOM.input({
'type': 'checkbox',
'checked': this.props.isRecordToImportCallback(aCard),
'onChange': MochiKit.Base.partial(this.props.toggleRecordToImportCallback,aCard)
'checked': this.isRecordToImport(aCard),
'onChange': MochiKit.Base.partial(this.toggleRecordToImport,aCard)
}),
React.DOM.h3({}, Clipperz.PM.DataModel.Record.filterOutTags(aCard.label)),
// REMOVE THE PREVIOUS LINE AND UNCOMMENT THE FOLLOWING 2 AFTER MERGE
// React.DOM.h3({}, Clipperz.PM.DataModel.Record.extractLabelFromFullLabel(aCard.label)),
// this.getTags(aCard.label),
React.DOM.h3({}, Clipperz.PM.DataModel.Record.extractLabelFromFullLabel(aCard.label)),
this.getTags(aCard.label),
React.DOM.dl({'className': 'fields'}, this.renderCardFields(aCard.currentVersion.fields)),
notesParagraph
]);
@@ -80,38 +131,17 @@ Clipperz.PM.UI.Components.ExtraFeatures.DataImport.PreviewClass = React.createCl
render: function() {
var result;
if (! this.props.importState.importData || typeof(this.props.importState.jsonToImport)=='undefined' || !this.props.importState.jsonToImport) {
if (typeof(this.state.jsonToImport)=='undefined' || !this.state.jsonToImport) {
result = "Error";
} else {
var renderedPreview = React.DOM.ul({},
MochiKit.Base.map(this.renderCard, this.props.importState.jsonToImport)
MochiKit.Base.map(this.renderCard, this.state.jsonToImport)
);
result = [
React.DOM.h2({},"Preview"),
Clipperz.PM.UI.Components.ExtraFeatures.DataImport.StepsNavigation({
'format': this.props.importState.importData.format,
'stepId': 'preview'
}),
React.DOM.button({
'onClick': MochiKit.Base.partial(this.props.goToStepCallback, this.props.importState.previousStep)}, "Back"),
React.DOM.span({}, " - "),
React.DOM.button({
'onClick': MochiKit.Base.bind(function() {
var filteredImportData = MochiKit.Base.filter(
MochiKit.Base.bind(function(r) {
return this.props.isRecordToImportCallback(r);
}, this),
this.props.importState.jsonToImport
);
MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'importCards', filteredImportData);
this.props.resetImportStateCallback();
}, this)
}, "Import"),
React.DOM.div({'className': 'jsonPreview'},renderedPreview),
];
result =
React.DOM.div({'className': 'jsonPreview'}, React.DOM.ul({},
MochiKit.Base.map(this.renderCard, this.state.jsonToImport)
) );
}
return React.DOM.div({},result);

View File

@@ -1,99 +0,0 @@
/*
Copyright 2008-2015 Clipperz Srl
This file is part of Clipperz, the online password manager.
For further information about its features and functionalities please
refer to http://www.clipperz.com.
* Clipperz is free software: you can redistribute it and/or modify it
under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
* Clipperz is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU Affero General Public License for more details.
* You should have received a copy of the GNU Affero General Public
License along with Clipperz. If not, see http://www.gnu.org/licenses/.
*/
"use strict";
Clipperz.Base.module('Clipperz.PM.UI.Components.ExtraFeatures.DataImport');
Clipperz.PM.UI.Components.ExtraFeatures.DataImport.StepsNavigationClass = React.createClass({
_stepsInfo: [
{
id: 'input',
name: 'Input',
formats: ['json', 'csv']
},
{
id: 'csv-columns',
name: 'Columns',
formats: ['csv']
},
{
id: 'csv-labels',
name: 'Labels',
formats: ['csv']
},
{
id: 'csv-titles',
name: 'Titles',
formats: ['csv']
},
{
id: 'csv-notes',
name: 'Notes',
formats: ['csv']
},
{
id: 'csv-hidden',
name: 'Hidden',
formats: ['csv']
},
{
id: 'preview',
name: 'Preview',
formats: ['json', 'csv']
}
],
render: function() {
var navigationButtons;
if (this.props.prevStep && this.props.nextStep) {
navigationButtons = [
React.DOM.button({'onClick': MochiKit.Base.partial(this.props.goToStepCallback, this.props.prevStep)}, "Back"),
React.DOM.span({}, " - "),
React.DOM.button({'onClick': MochiKit.Base.partial(this.props.goToStepCallback, this.props.nextStep), 'disabled': this.props.nextDisabled }, "Next")
];
} else {
}
return React.DOM.div({},[
React.DOM.ul({'className': 'stepsOverview'},
MochiKit.Base.map(MochiKit.Base.bind(function(aStep) {
var className;
className = (aStep.id == this.props.stepId) ? 'active' : 'inactive';
className = (MochiKit.Base.findValue(aStep.formats,this.props.format)>= 0) ? className+' enabled' : className+' disabled';
// TODO: replace with proper CSS
var style = (aStep.id == this.props.stepId) ? {'display': 'inline-block', 'textDecoration': 'underline'} : {'display': 'inline-block'};
return React.DOM.li({'className': className, 'style': style}, aStep.name);
},this), this._stepsInfo)
),
navigationButtons
]);
}
});
Clipperz.PM.UI.Components.ExtraFeatures.DataImport.StepsNavigation = React.createFactory(Clipperz.PM.UI.Components.ExtraFeatures.DataImport.StepsNavigationClass);

View File

@@ -43,7 +43,6 @@ Clipperz.PM.UI.Components.ExtraFeatures.DeleteAccountClass = React.createClass({
handleDeleteAccount: function(event) {
event.preventDefault();
MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'deleteAccount');
},
@@ -77,14 +76,13 @@ Clipperz.PM.UI.Components.ExtraFeatures.DeleteAccountClass = React.createClass({
render: function () {
return React.DOM.div({className:'extraFeature deleteAccount'}, [
React.DOM.h1({}, "Delete Account"),
React.DOM.div({'className': 'content'}, [
React.DOM.form({'key':'form', 'className':'deleteAccountForm', 'onChange': this.handleFormChange, 'onSubmit':this.handleDeleteAccount}, [
React.DOM.div({'key':'fields'},[
React.DOM.label({'key':'username-label', 'htmlFor' :'name'}, "username"),
React.DOM.input({'key':'username', 'className': this.state['username'], 'type':'text', 'name':'name', 'ref':'username', 'placeholder':"username", 'autoCapitalize':'none'}),
React.DOM.span({'className': 'invalidMsg'},'Invalid username!'),
React.DOM.label({'key':'passphrase-label', 'autoFocus': 'true', 'htmlFor' :'passphrase'}, "passphrase"),
React.DOM.input({'key':'passphrase', 'className': this.state['passphrase'], 'type':'password', 'name':'passphrase', 'ref':'passphrase', 'placeholder':"passphrase"}),
React.DOM.span({'className': 'invalidMsg'},'Invalid passphrase!'),
React.DOM.p({}, [
React.DOM.input({'key':'confirm', 'className':'confirmCheckbox', 'type':'checkbox', 'name':'confirm', 'ref':'confirm'}),
React.DOM.span({}, "I understand that all my data will be deleted and that this action is irreversible.")
@@ -92,6 +90,7 @@ Clipperz.PM.UI.Components.ExtraFeatures.DeleteAccountClass = React.createClass({
]),
React.DOM.button({'key':'button', 'type':'submit', 'disabled':!this.shouldEnableDeleteAccountButton(), 'className':'button'}, "Delete my account")
])
])
]);
},

View File

@@ -36,7 +36,9 @@ Clipperz.PM.UI.Components.ExtraFeatures.DevicePINClass = React.createClass({
render: function () {
return React.DOM.div({className:'extraFeature devicePIN'}, [
React.DOM.h1({}, "Device PIN"),
React.DOM.h3({}, this.props['PIN'])
React.DOM.div({'className': 'content'}, [
React.DOM.h3({}, this.props['PIN'])
])
]);
},

View File

@@ -1,409 +0,0 @@
/*
Copyright 2008-2015 Clipperz Srl
This file is part of Clipperz, the online password manager.
For further information about its features and functionalities please
refer to http://www.clipperz.com.
* Clipperz is free software: you can redistribute it and/or modify it
under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
* Clipperz is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU Affero General Public License for more details.
* You should have received a copy of the GNU Affero General Public
License along with Clipperz. If not, see http://www.gnu.org/licenses/.
*/
"use strict";
Clipperz.Base.module('Clipperz.PM.UI.Components.ExtraFeatures');
Clipperz.PM.UI.Components.ExtraFeatures.DataImportClass = React.createClass({
getInitialState: function() {
return {
'currentStep': 'input',
'importData': {'input': ""},
'recordsToImport': null
};
},
goToStep: function(aStep) {
this.setState({'currentStep': aStep});
},
resetState: function() {
this.replaceState( this.getInitialState() );
},
//=========================================================================
addImportIds: function (someJson) {
var count;
for (count=0; count < someJson.length; count++) {
someJson[count]['_importId'] = count;
}
},
toggleRecordToImport: function(record) {
var newRecordsToImport;
var recordPosition;
newRecordsToImport = this.state.recordsToImport;
recordPosition = newRecordsToImport.indexOf(record._importId);
if (recordPosition === -1) {
newRecordsToImport.push(record._importId);
} else {
newRecordsToImport.splice(recordPosition,1);
}
this.setState({'recordsToImport': newRecordsToImport});
},
isRecordToImport: function(record) {
return (this.state.recordsToImport.indexOf(record._importId)>=0) ? true : false;
},
extractJsonFromClipperzExport: function(someHtml) {
var temporaryTextarea;
var regexMatch;
var result;
// Should move the regex to global?
var re = new RegExp('.*<textarea>(.*)<\/textarea>.*','g');
if (re.test(someHtml)) {
// Needed to escape HTML entities
temporaryTextarea = document.createElement('textarea');
temporaryTextarea.innerHTML = someHtml.replace(re, '$1');
result = temporaryTextarea.innerHTML;
} else {
result = false;
}
return result;
},
parseJson: function(aJsonString) {
var result;
var jsonData;
try {
jsonData = JSON.parse(aJsonString);
this.addImportIds(jsonData);
result = {
'importData': {
'format': 'json',
'input': aJsonString,
},
'jsonToImport': jsonData,
'recordsToImport': jsonData.map(function(d){return d._importId}),
'currentStep': 'preview',
'previousStep': 'input'
};
} catch(e) {
result = false;
}
return result;
},
parseCsv: function(aCsvString) {
var result;
var parsedCSV;
var nColumns;
var defaultSelectedColumns;
var defaultHiddenColumns;
var defaultColumnLabels;
var columnLabelsFirstrow;
var i;
var papaParsedCSV = Papa.parse(aCsvString);
event.preventDefault();
if (papaParsedCSV.errors.length != 0) {
result = false;
} else {
parsedCSV = this.csvFillEmptyCells(papaParsedCSV.data);
nColumns = parsedCSV[0].length;
defaultSelectedColumns = {};
defaultHiddenColumns = {};
defaultColumnLabels = {};
columnLabelsFirstrow = {};
for (i=0; i<nColumns; i++) {
defaultSelectedColumns[i] = true;
defaultHiddenColumns[i] = false;
defaultColumnLabels[i] = "";
columnLabelsFirstrow[i] = parsedCSV[0][i];
}
result = {
'importData': {
'format': 'csv',
'input': aCsvString,
'parsedCSV': parsedCSV,
'nColumns': nColumns,
'selectedColumns': defaultSelectedColumns,
'firstRowAsLabels': false,
'columnLabels': defaultColumnLabels,
'columnLabelsFirstrow': columnLabelsFirstrow,
'titlesColumn': null,
'notesColumn': null,
'hiddenColumns': defaultHiddenColumns,
'json': []
},
'currentStep': 'csv-columns'
};
}
return result;
},
csvFillEmptyCells: function(table) {
var i,j;
var result = [];
var maxColumns = MochiKit.Iter.reduce(function(prev,next) {
return Math.max(prev,next)
}, MochiKit.Base.map(function(row) {return row.length;}, table) );
for (i=0; i<table.length; i++) {
result[i] = [];
for (j=0; j<maxColumns; j++) {
result[i][j] = (typeof(table[i][j]) != "undefined") ? table[i][j] : "";
}
}
return result;
},
csvGetColumnLabels: function() {
return (this.state.importData.firstRowAsLabels) ? this.state.importData.columnLabelsFirstrow : this.state.importData.columnLabels;
},
csvToJson: function() {
var result;
var importData = this.state.importData;
var columnLabels = this.csvGetColumnLabels();
result = [];
for (rowCount=0; rowCount<importData.parsedCSV.length; rowCount++) {
var rowCount,cellCount;
if (rowCount != 0 || ! importData.firstRowAsLabels) {
var record;
record = {};
record._importId = rowCount;
record.label = importData.parsedCSV[rowCount][importData.titlesColumn];
record.data = {'notes': ""};
record.currentVersion = {'fields': {}};
for (cellCount=0; cellCount<importData.parsedCSV[rowCount].length; cellCount++) {
if (importData.selectedColumns[cellCount] && cellCount != importData.notesColumn && cellCount != importData.titlesColumn) {
var fieldKey = rowCount+"-"+cellCount;
var field = {
'label': columnLabels[cellCount],
'value': importData.parsedCSV[rowCount][cellCount],
'hidden': importData.hiddenColumns[cellCount]
};
record.currentVersion.fields[fieldKey] = field;
} else if (cellCount == importData.notesColumn) {
record.data.notes = importData.parsedCSV[rowCount][cellCount];
}
}
result.push(record);
}
}
return result;
},
//=========================================================================
csvRenderTbody: function() {
var rowCount;
var cellCount;
var firstRowAsLabels = this.state.importData.firstRowAsLabels;
var selectedColumns = this.state.importData.selectedColumns;
rowCount = 0;
return MochiKit.Base.map(function(row){
var result;
cellCount = 0;
if (rowCount == 0 && firstRowAsLabels) {
result = null;
} else {
result = React.DOM.tr({'key': 'csv-row-'+(rowCount)}, MochiKit.Base.map(function(cell) {
return (selectedColumns[cellCount]) ? React.DOM.td({'key': 'csv-cell-'+rowCount+'-'+(cellCount++)},cell) : null;
}, row));
}
rowCount++;
return result;
}, this.state.importData.parsedCSV);
},
csvRenderTheadInput: function(stepName, inputType, valueCallback, onChange, disabledCallback, showLabels) {
var cellCount;
var importData = this.state.importData;
cellCount = 0;
return React.DOM.tr({},
MochiKit.Base.map(MochiKit.Base.bind(function(cell) {
var result;
var columnLabels = (importData.firstRowAsLabels) ? importData.columnLabelsFirstrow : importData.columnLabels;
var inputLabel = (showLabels) ? React.DOM.label({'htmlFor': 'csv-'+stepName+'-input-'+cellCount}, columnLabels[cellCount]) : null;
if (! importData.selectedColumns[cellCount]) {
result = null;
} else {
var inputProps = {
'type': inputType,
'id': 'csv-'+stepName+'-input-'+cellCount,
'key': 'csv-'+stepName+'-input-'+cellCount,
'ref': 'csv-'+stepName+'-input-'+cellCount,
'onChange': MochiKit.Base.partial(onChange,cellCount)
}
if (inputType == 'radio' || inputType == 'checkbox') {
inputProps['checked'] = MochiKit.Base.partial(valueCallback,cellCount)();
} else {
inputProps['value'] = MochiKit.Base.partial(valueCallback,cellCount)();
}
if (disabledCallback) {
inputProps['disabled'] = MochiKit.Base.partial(disabledCallback,cellCount)();
}
result = React.DOM.th({'key': 'csv-'+stepName+'-header-'+cellCount}, [
inputLabel,
React.DOM.input(inputProps)
]);
}
cellCount++;
return result;
}, this), this.state.importData.parsedCSV[0])
)
},
setStateCB: function(aState) {
this.setState(aState);
},
_renderStepMethods: {
'input': function() {
return new Clipperz.PM.UI.Components.ExtraFeatures.DataImport.Input({
'importState': this.state,
'setImportStateCallback': this.setStateCB,
'goToStepCallback': this.goToStep,
'extractJsonFromClipperzExportCallback': this.extractJsonFromClipperzExport,
'parseJsonCallback': this.parseJson,
'parseCsvCallback': this.parseCsv
});
},
'csv-columns': function() {
return new Clipperz.PM.UI.Components.ExtraFeatures.DataImport.CsvColumns({
'importState': this.state,
'setImportStateCallback': this.setStateCB,
'goToStepCallback': this.goToStep,
});
},
'csv-labels': function() {
return new Clipperz.PM.UI.Components.ExtraFeatures.DataImport.CsvLabels({
'importState': this.state,
'setImportStateCallback': this.setStateCB,
'goToStepCallback': this.goToStep,
'csvRenderTheadInputCallback': this.csvRenderTheadInput,
'csvRenderTbodyCallback': this.csvRenderTbody,
'csvGetColumnLabelsCallback': this.csvGetColumnLabels
});
},
'csv-titles': function() {
return new Clipperz.PM.UI.Components.ExtraFeatures.DataImport.CsvTitles({
'importState': this.state,
'setImportStateCallback': this.setStateCB,
'goToStepCallback': this.goToStep,
'csvRenderTheadInputCallback': this.csvRenderTheadInput,
'csvRenderTbodyCallback': this.csvRenderTbody,
});
},
'csv-notes': function() {
return new Clipperz.PM.UI.Components.ExtraFeatures.DataImport.CsvNotes({
'importState': this.state,
'setImportStateCallback': this.setStateCB,
'goToStepCallback': this.goToStep,
'csvRenderTheadInputCallback': this.csvRenderTheadInput,
'csvRenderTbodyCallback': this.csvRenderTbody,
});
},
'csv-hidden': function() {
return new Clipperz.PM.UI.Components.ExtraFeatures.DataImport.CsvHidden({
'importState': this.state,
'setImportStateCallback': this.setStateCB,
'goToStepCallback': this.goToStep,
'csvRenderTheadInputCallback': this.csvRenderTheadInput,
'csvRenderTbodyCallback': this.csvRenderTbody,
'csvToJsonCallback': this.csvToJson
});
},
//-------------------------------------------------------------------------
'preview': function() {
return new Clipperz.PM.UI.Components.ExtraFeatures.DataImport.Preview({
'importState': this.state,
'resetImportStateCallback': this.resetState,
'goToStepCallback': this.goToStep,
'isRecordToImportCallback': this.isRecordToImport,
'toggleRecordToImportCallback': this.toggleRecordToImport
});
},
},
//=========================================================================
renderStep: function(step) {
return MochiKit.Base.method(this, this._renderStepMethods[step])();
},
render: function () {
return React.DOM.div({className:'extraFeature'}, [
React.DOM.h1({}, "Import"),
this.renderStep(this.state.currentStep)
]);
},
//=========================================================================
});
Clipperz.PM.UI.Components.ExtraFeatures.DataImport = React.createFactory(Clipperz.PM.UI.Components.ExtraFeatures.DataImportClass);

View File

@@ -27,96 +27,103 @@ Clipperz.Base.module('Clipperz.PM.UI.Components.ExtraFeatures');
Clipperz.PM.UI.Components.ExtraFeatures.PassphraseClass = React.createClass({
propTypes: {
// featureSet: React.PropTypes.oneOf(['FULL', 'EXPIRED', 'TRIAL']).isRequired,
// 'level': React.PropTypes.oneOf(['hide', 'info', 'warning', 'error']).isRequired
},
getInitialState: function() {
return {
'username': '',
'old-passphrase': '',
'new-passphrase': '',
'confirm-new-passphrase': '',
'error': ''
'username': 'empty',
'old-passphrase': 'empty',
'new-passphrase': 'empty',
'confirm-new-passphrase': 'empty',
'confirm': '',
};
},
//=========================================================================
shouldEnableChangePassphraseButton: function() {
return (
this.state['username'] &&
this.state['old-passphrase'] &&
this.state['new-passphrase'] &&
this.state['confirm-new-passphrase'] &&
(this.state['new-passphrase'] == this.state['confirm-new-passphrase'])
);
},
handleFormChange: function() {
this.setState({
'username': this.refs['username'].getDOMNode().value,
'old-passphrase': this.refs['old-passphrase'].getDOMNode().value,
'new-passphrase': this.refs['new-passphrase'].getDOMNode().value,
'confirm-new-passphrase': this.refs['confirm-new-passphrase'].getDOMNode().value
});
resetForm: function () {
this.setState(this.getInitialState());
this.refs['username'].getDOMNode().value = '';
this.refs['old-passphrase'].getDOMNode().value = '';
this.refs['new-passphrase'].getDOMNode().value = '';
this.refs['confirm-new-passphrase'].getDOMNode().value = '';
this.refs['confirm'].getDOMNode().checked = false;
},
handleChangePassphrase: function(event) {
event.preventDefault();
if (this.refs['username'].getDOMNode().value != this.props.userInfo['username']) {
this.setState({error: "Invalid username"});
return;
}
var newPassphrase;
event.preventDefault();
newPassphrase = this.refs['new-passphrase'].getDOMNode().value;
this.resetForm();
MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'changePassphrase', newPassphrase);
},
handleFormChange: function() {
var deferredResult;
deferredResult = new Clipperz.Async.Deferred("Passphrase.handleChangePassphrase", {trace: false});
deferredResult = new Clipperz.Async.Deferred("Passphrase.handleFormChange", {trace: false});
deferredResult.addCallback(this.props.userInfo['checkPassphraseCallback'], this.refs['old-passphrase'].getDOMNode().value);
deferredResult.addIf(
[
MochiKit.Base.partial(MochiKit.Signal.signal, Clipperz.Signal.NotificationCenter, 'changePassphrase', this.refs['new-passphrase'].getDOMNode().value),
MochiKit.Base.method(this, function() {
this.refs['username'].getDOMNode().value = '';
this.refs['old-passphrase'].getDOMNode().value = '';
this.refs['new-passphrase'].getDOMNode().value = '';
this.refs['confirm-new-passphrase'].getDOMNode().value = '';
this.setState({'error': ''});
})
],
[MochiKit.Base.bind(this.setState, this, {error: "Invalid password"})]
);
deferredResult.addMethod(this, function(passCheck){
var username = this.refs['username'].getDOMNode().value;
var oldPassphrase = this.refs['old-passphrase'].getDOMNode().value;
var newPassphrase = this.refs['new-passphrase'].getDOMNode().value;
var confirmNewPassphrase = this.refs['confirm-new-passphrase'].getDOMNode().value;
this.setState({
'username': (username != '') ? [(username == this.props.userInfo['username']) ? 'valid' : 'invalid'] : 'empty',
'old-passphrase': (oldPassphrase != '') ? [(passCheck) ? 'valid' : 'invalid'] : 'empty',
'new-passphrase': (newPassphrase != '') ? 'valid' : 'empty',
'confirm-new-passphrase': (confirmNewPassphrase != '') ? [(confirmNewPassphrase == newPassphrase) ? 'valid' : 'invalid'] : 'empty',
'confirm': this.refs['confirm'].getDOMNode().checked,
});
});
deferredResult.callback();
return deferredResult;
// MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'changePassphrase', this.refs['new-passphrase'].getDOMNode().value);
},
shouldEnableChangePassphraseButton: function() {
return (
this.state['username'] == 'valid' &&
this.state['old-passphrase'] == 'valid' &&
this.state['new-passphrase'] == 'valid' &&
this.state['confirm-new-passphrase'] == 'valid' &&
this.state['confirm']
);
},
//=========================================================================
render: function () {
var errorVisibility = (this.state.error) ? 'visible' : 'hidden';
return React.DOM.div({className:'extraFeature passphrase'}, [
React.DOM.h1({}, "Change Passphrase"),
React.DOM.form({'key':'form', 'className':'changePassphraseForm', 'onChange': this.handleFormChange, 'onSubmit':this.handleChangePassphrase}, [
React.DOM.div({'key':'fields'},[
React.DOM.label({'key':'username-label', 'htmlFor' :'name'}, "username"),
React.DOM.input({'key':'username', 'type':'text', 'name':'name', 'ref':'username', 'placeholder':"username", 'autoCapitalize':'none'}),
React.DOM.label({'key':'old-passphrase-label', 'htmlFor' :'old-passphrase'}, "old passphrase"),
React.DOM.input({'key':'old-passphrase', 'type':'password', 'name':'old-passphrase', 'ref':'old-passphrase', 'placeholder':"old passphrase"}),
React.DOM.label({'key':'new-passphrase-label', 'autoFocus': 'true', 'htmlFor' :'new-passphrase'}, "new passphrase"),
React.DOM.input({'key':'new-passphrase', 'type':'password', 'name':'new-passphrase', 'ref':'new-passphrase', 'placeholder':"new passphrase"}),
React.DOM.label({'key':'confirm-new-passphrase-label', 'htmlFor' :'confirm-new-passphrase'}, "confirm new passphrase"),
React.DOM.input({'key':'confirm-new-passphrase', 'type':'password', 'name':'confirm-new-passphrase', 'ref':'confirm-new-passphrase', 'placeholder':"confirm new passphrase"})
]),
React.DOM.button({'key':'button', 'type':'submit', 'disabled':!this.shouldEnableChangePassphraseButton(), 'className':'button'}, "Change"),
React.DOM.div({ref: 'errorMessage', className: 'errorMessage', style: {visibility: errorVisibility} }, this.state.error)
]),
React.DOM.div({'className': 'content'}, [
React.DOM.form({'key':'form', 'className':'changePassphraseForm', 'onChange': this.handleFormChange, 'onSubmit':this.handleChangePassphrase}, [
React.DOM.div({'key':'fields'},[
React.DOM.label({'key':'username-label', 'htmlFor' :'name'}, "username"),
React.DOM.input({'key':'username', 'className':this.state['username'], 'type':'text', 'name':'name', 'ref':'username', 'placeholder':"username", 'autoCapitalize':'none'}),
React.DOM.label({'key':'old-passphrase-label', 'htmlFor' :'old-passphrase'}, "old passphrase"),
React.DOM.input({'key':'old-passphrase', 'className':this.state['old-passphrase'], 'type':'password', 'name':'old-passphrase', 'ref':'old-passphrase', 'placeholder':"old passphrase"}),
React.DOM.label({'key':'new-passphrase-label', 'autoFocus': 'true', 'htmlFor' :'new-passphrase'}, "new passphrase"),
React.DOM.input({'key':'new-passphrase', 'className':this.state['new-passphrase'], 'type':'password', 'name':'new-passphrase', 'ref':'new-passphrase', 'placeholder':"new passphrase"}),
React.DOM.label({'key':'confirm-new-passphrase-label', 'htmlFor' :'confirm-new-passphrase'}, "confirm new passphrase"),
React.DOM.input({'key':'confirm-new-passphrase', 'className':this.state['confirm-new-passphrase'], 'type':'password', 'name':'confirm-new-passphrase', 'ref':'confirm-new-passphrase', 'placeholder':"confirm new passphrase"}),
React.DOM.p({}, [
React.DOM.input({'key':'confirm', 'className':'confirmCheckbox', 'type':'checkbox', 'name':'confirm', 'ref':'confirm'}),
React.DOM.span({}, "I understand that Clipperz will not be able to recover a lost passphrase.")
]),
]),
React.DOM.button({'key':'button', 'type':'submit', 'disabled':!this.shouldEnableChangePassphraseButton(), 'className':'button'}, "Change passphrase"),
])
])
]);
},