diff --git a/frontend/delta/js/Clipperz/PM/DataModel/Record.js b/frontend/delta/js/Clipperz/PM/DataModel/Record.js index 4aa0d04..ddff8c6 100644 --- a/frontend/delta/js/Clipperz/PM/DataModel/Record.js +++ b/frontend/delta/js/Clipperz/PM/DataModel/Record.js @@ -155,12 +155,8 @@ Clipperz.Base.extend(Clipperz.PM.DataModel.Record, Clipperz.PM.DataModel.Encrypt //............................................................................ - 'tagChar': function () { - return Clipperz.PM.DataModel.Record.tagChar; - }, - 'tagRegExp': function () { - return new RegExp('\\' + this.tagChar() + '(\\w+)', 'g'); + return new RegExp('\\' + Clipperz.PM.DataModel.Record.tagChar + '(' + Clipperz.PM.DataModel.Record.specialTagChar + '?\\w+)', 'g'); }, 'trimSpacesRegExp': function () { @@ -1006,14 +1002,23 @@ Clipperz.PM.DataModel.Record.defaultCardInfo = { Clipperz.PM.DataModel.Record.defaultSearchField = '_searchableContent'; Clipperz.PM.DataModel.Record.tagChar = '\uE009'; -Clipperz.PM.DataModel.Record.specialTagsWrapper = '___'; +Clipperz.PM.DataModel.Record.specialTagChar = '\uE010'; Clipperz.PM.DataModel.Record.specialTagsConstructor = function (aTag) { - return Clipperz.PM.DataModel.Record.specialTagsWrapper + aTag + Clipperz.PM.DataModel.Record.specialTagsWrapper; + return Clipperz.PM.DataModel.Record.specialTagChar + aTag; } -Clipperz.PM.DataModel.Record.archivedTag = Clipperz.PM.DataModel.Record.specialTagsConstructor('archived'); +Clipperz.PM.DataModel.Record.archivedTag = Clipperz.PM.DataModel.Record.specialTagsConstructor('ARCH'); Clipperz.PM.DataModel.Record.regExpForTag = function (aTag) { return new RegExp('\\' + Clipperz.PM.DataModel.Record.tagChar + aTag, 'g'); }; +Clipperz.PM.DataModel.Record.regExpForNoTag = function () { + return new RegExp('^((?!\\' + Clipperz.PM.DataModel.Record.tagChar + '[^' + Clipperz.PM.DataModel.Record.specialTagChar + ']).)*$', 'g'); +} +Clipperz.PM.DataModel.Record.isSpecialTag = function (aTag) { + return aTag.indexOf(Clipperz.PM.DataModel.Record.specialTagChar) == 0; +}; +Clipperz.PM.DataModel.Record.isRegularTag = function (aTag) { + return !Clipperz.PM.DataModel.Record.isSpecialTag(aTag); +}; Clipperz.PM.DataModel.Record.regExpForSearch = function (aSearch) { return new RegExp(aSearch.replace(/[^A-Za-z0-9]/g, '\\$&'), 'i'); }; diff --git a/frontend/delta/js/Clipperz/PM/DataModel/User.js b/frontend/delta/js/Clipperz/PM/DataModel/User.js index 290fba0..6795c87 100644 --- a/frontend/delta/js/Clipperz/PM/DataModel/User.js +++ b/frontend/delta/js/Clipperz/PM/DataModel/User.js @@ -509,11 +509,18 @@ Clipperz.Base.extend(Clipperz.PM.DataModel.User, Object, { //========================================================================= - 'getTags': function () { + 'getTags': function (shouldIncludeArchivedCards) { + var archivedCardsFilter = (shouldIncludeArchivedCards || false) + ? MochiKit.Async.succeed + : MochiKit.Base.partial(MochiKit.Base.filter, function (someTags) { + return someTags.indexOf(Clipperz.PM.DataModel.Record.archivedTag) == -1; + }); + return Clipperz.Async.callbacks("User.getTags", [ MochiKit.Base.method(this, 'getRecords'), MochiKit.Base.partial(MochiKit.Base.map, MochiKit.Base.methodcaller('tags')), Clipperz.Async.collectAll, + archivedCardsFilter, MochiKit.Base.flattenArray, MochiKit.Iter.groupby, function (someGroups) { @@ -556,6 +563,7 @@ Clipperz.Base.extend(Clipperz.PM.DataModel.User, Object, { ], {trace:false}); }, + /* 'filterRecordsInfo': function (someArgs) { var info = (someArgs.info ? someArgs.info : Clipperz.PM.DataModel.Record.defaultCardInfo); diff --git a/frontend/delta/js/Clipperz/PM/UI/Components/CardToolbar.js b/frontend/delta/js/Clipperz/PM/UI/Components/CardToolbar.js index 0dc0404..36d8746 100644 --- a/frontend/delta/js/Clipperz/PM/UI/Components/CardToolbar.js +++ b/frontend/delta/js/Clipperz/PM/UI/Components/CardToolbar.js @@ -63,8 +63,10 @@ Clipperz.PM.UI.Components.CardToolbar = React.createClass({ renderWithoutSidePanels: function () { var result; - + if (this.props['filter']) { +//console.log("CARD TOOLBAR", this.props['filter']['type']); + if (this.props['filter']['type'] == 'RECENT') { result = [React.DOM.div({className:'clipperz'}, [React.DOM.span({className:'logo recent'}, "recent")])]; } else if (this.props['filter']['type'] == 'TAG') { @@ -72,6 +74,11 @@ Clipperz.PM.UI.Components.CardToolbar = React.createClass({ React.DOM.span({className:'logo tag'}, "tag"), React.DOM.span({className:'value'}, this.props['filter']['value']) ])]; + } else if (this.props['filter']['type'] == 'UNTAGGED') { + result = [React.DOM.div({className:'clipperz'}, [ + React.DOM.span({className:'logo tag'}, "tag"), + React.DOM.span({className:'value'}, "untagged") + ])]; } else if (this.props['filter']['type'] == 'SEARCH') { result = [React.DOM.div({className:'clipperz'}, [ React.DOM.span({className:'logo search'}, "search"), diff --git a/frontend/delta/js/Clipperz/PM/UI/Components/Cards/View.js b/frontend/delta/js/Clipperz/PM/UI/Components/Cards/View.js index 09dea2c..18e078a 100644 --- a/frontend/delta/js/Clipperz/PM/UI/Components/Cards/View.js +++ b/frontend/delta/js/Clipperz/PM/UI/Components/Cards/View.js @@ -83,7 +83,10 @@ Clipperz.PM.UI.Components.Cards.View = React.createClass({ }, renderTags: function (someTags) { - return React.DOM.div({'className':'cardTags'}, MochiKit.Base.map(this.renderTag, someTags)); + var tags; + + tags = MochiKit.Base.filter(Clipperz.PM.DataModel.Record.isRegularTag, someTags).sort(Clipperz.Base.caseInsensitiveCompare); + return React.DOM.div({'className':'cardTags'}, MochiKit.Base.map(this.renderTag, tags)); }, //............................................................................ diff --git a/frontend/delta/js/Clipperz/PM/UI/Components/DialogBox.js b/frontend/delta/js/Clipperz/PM/UI/Components/DialogBox.js new file mode 100644 index 0000000..e071afe --- /dev/null +++ b/frontend/delta/js/Clipperz/PM/UI/Components/DialogBox.js @@ -0,0 +1,51 @@ +/* + +Copyright 2008-2013 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/. + +*/ + +Clipperz.Base.module('Clipperz.PM.UI.Components'); + +Clipperz.PM.UI.Components.DialogBox = React.createClass({ + + propTypes: { + 'level': React.PropTypes.oneOf(['HIDE', 'INFO', 'WARNING', 'ERROR']).isRequired, + 'message': React.PropTypes.string.isRequired, + }, + + ask: function (someInfo) { + var deferredResult; + + deferredResult = new Clipperz.Async.Deferred('DialogBox.ask', {trace:false}); + deferredResult.addCallback(someInfo.['possibleAnswers']['cancel']['answer']); + deferredResult.callback(); +// deferredResult.cancel(); + + return deferredResult; + }, + + //========================================================================= + + render: function () { + return React.DOM.div({className:'messageBox ' + this.props['level']}, this.props['message']); + } + + //========================================================================= +}); diff --git a/frontend/delta/js/Clipperz/PM/UI/Components/Pages/MainPage.js b/frontend/delta/js/Clipperz/PM/UI/Components/Pages/MainPage.js index c134f1d..0d258af 100644 --- a/frontend/delta/js/Clipperz/PM/UI/Components/Pages/MainPage.js +++ b/frontend/delta/js/Clipperz/PM/UI/Components/Pages/MainPage.js @@ -53,7 +53,6 @@ Clipperz.PM.UI.Components.Pages.MainPage = React.createClass({ 'mainPage': true }; classes[this.props['style']] = true; -//console.log("MainPage.cards", this.props['cards'], this.props['cards'].state()); return React.DOM.div({className:React.addons.classSet(classes)}, [ this.props['style'] != 'extra-wide' ? Clipperz.PM.UI.Components.Panels.SelectionPanel(this.props) : null, diff --git a/frontend/delta/js/Clipperz/PM/UI/Components/Panels/MainPanel.js b/frontend/delta/js/Clipperz/PM/UI/Components/Panels/MainPanel.js index fac9213..e0deb98 100644 --- a/frontend/delta/js/Clipperz/PM/UI/Components/Panels/MainPanel.js +++ b/frontend/delta/js/Clipperz/PM/UI/Components/Panels/MainPanel.js @@ -171,7 +171,7 @@ Clipperz.PM.UI.Components.Panels.MainPanel = React.createClass({ }, render: function () { -//console.log("MainPanel.cards", this.props['cards']); +//console.log("MainPanel.cards", this.props['ask']); var classes = { 'panel': true, 'left': this.props['selectionPanelStatus'] == 'OPEN', diff --git a/frontend/delta/js/Clipperz/PM/UI/Components/Selections.js b/frontend/delta/js/Clipperz/PM/UI/Components/Selections.js index 868a6b8..4f365ca 100644 --- a/frontend/delta/js/Clipperz/PM/UI/Components/Selections.js +++ b/frontend/delta/js/Clipperz/PM/UI/Components/Selections.js @@ -36,8 +36,12 @@ Clipperz.PM.UI.Components.Selections = React.createClass({ MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'selectRecentCards'); }, + selectUntaggedCards: function (anEvent) { + MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'selectUntaggedCards'); + }, + handleCheckboxChanges: function (anEvent) { - if (anEvent.target.checked) { + if (!this.props['shouldIncludeArchivedCards']) { MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'showArchivedCards'); } else { MochiKit.Signal.signal(Clipperz.Signal.NotificationCenter, 'hideArchivedCards'); @@ -50,14 +54,16 @@ Clipperz.PM.UI.Components.Selections = React.createClass({ var archivedCardsCount; tagInfo = this.props['tags'] ? this.props['tags'] : {}; - tags = MochiKit.Base.filter(function (aTag) { return aTag != Clipperz.PM.DataModel.Record.archivedTag}, MochiKit.Base.keys(tagInfo)).sort(Clipperz.Base.caseInsensitiveCompare); - - archivedCardsCount = tagInfo[Clipperz.PM.DataModel.Record.archivedTag] ? tagInfo[Clipperz.PM.DataModel.Record.archivedTag] : 0; + tags = MochiKit.Base.filter(Clipperz.PM.DataModel.Record.isRegularTag, MochiKit.Base.keys(tagInfo)).sort(Clipperz.Base.caseInsensitiveCompare); + +// archivedCardsCount = tagInfo[Clipperz.PM.DataModel.Record.archivedTag] ? tagInfo[Clipperz.PM.DataModel.Record.archivedTag] : 0; + archivedCardsCount = this.props['archivedCardsCount']; return React.DOM.div({'key':'selections', 'id':'selections'}, [ React.DOM.ul({'className':'defaultSet'}, [ React.DOM.li({'className':'allCards', onClick: this.selectAll}, "All"), - React.DOM.li({'className':'recentCards', onClick: this.selectRecent}, "Recent") + React.DOM.li({'className':'recentCards', onClick: this.selectRecent}, "Recent"), + React.DOM.li({'className':'untaggedCards', onClick: this.selectUntaggedCards}, "Untagged - " + this.props['untaggedCardsCount']) ]), React.DOM.div({'className':'search'}, [ React.DOM.form({'className':'searchForm'}, [ @@ -66,8 +72,8 @@ Clipperz.PM.UI.Components.Selections = React.createClass({ ]) ]), React.DOM.ul({'className':'tagList'}, MochiKit.Base.map(function (aTag) {return Clipperz.PM.UI.Components.TagIndexItem({'label':aTag, 'count':tagInfo[aTag]}); }, tags)), - React.DOM.div({'className':'showArchivedCards'}, [ - React.DOM.input({'type':'checkbox', 'onChange':this.handleCheckboxChanges}), + React.DOM.div({'className':'showArchivedCards', 'onClick':this.handleCheckboxChanges}, [ + React.DOM.input({'type':'checkbox', 'checked':this.props['shouldIncludeArchivedCards'] ? 'checked' : null}), React.DOM.span({'className':'label'}, "Show archived cards"), archivedCardsCount > 0 ? React.DOM.span({'className':'count'}, archivedCardsCount) : null ]), diff --git a/frontend/delta/js/Clipperz/PM/UI/MainController.js b/frontend/delta/js/Clipperz/PM/UI/MainController.js index baffc6b..65adce2 100644 --- a/frontend/delta/js/Clipperz/PM/UI/MainController.js +++ b/frontend/delta/js/Clipperz/PM/UI/MainController.js @@ -70,6 +70,7 @@ Clipperz.PM.UI.MainController = function() { 'selectAllCards', 'selectRecentCards', 'tagSelected', + 'selectUntaggedCards', 'cardSelected', @@ -536,6 +537,9 @@ console.log("SET USER", aUser); } else if (aFilter['type'] == 'TAG') { deferredResult.addCallback(MochiKit.Base.filter, this.regExpFilterGenerator(Clipperz.PM.DataModel.Record.regExpForTag(aFilter['value']))); deferredResult.addMethodcaller('sort', sortCriteria); + } else if (aFilter['type'] == 'UNTAGGED') { + deferredResult.addCallback(MochiKit.Base.filter, this.regExpFilterGenerator(Clipperz.PM.DataModel.Record.regExpForNoTag())); + deferredResult.addMethodcaller('sort', sortCriteria); } deferredResult.addMethod(this, 'setPageProperties', 'mainPage', 'cards'); @@ -574,6 +578,34 @@ console.log("SET USER", aUser); //---------------------------------------------------------------------------- + getArchivedCardsCount: function () { + return Clipperz.Async.callbacks("MainController.getArchivedCardsCount", [ + MochiKit.Base.method(this.user(), 'getRecords'), + MochiKit.Base.partial(MochiKit.Base.map, Clipperz.Async.collectResults("collectResults", {'_isArchived':MochiKit.Base.methodcaller('isArchived')}, {trace:false})), + Clipperz.Async.collectAll, + function (aResult) { + return MochiKit.Base.filter(function (aRecordInfo) { return aRecordInfo['_isArchived']; }, aResult).length; + } + ], {trace:false}); + }, + + getUntaggedCardsCount: function () { + var archivedCardsFilter = this.shouldIncludeArchivedCards() + ? MochiKit.Async.succeed + : MochiKit.Base.partial(MochiKit.Base.filter, function (someRecordInfo) { return ! someRecordInfo['_isArchived']; }); + + return Clipperz.Async.callbacks("MainController.getUntaggedCardsCount", [ + MochiKit.Base.method(this.user(), 'getRecords'), + MochiKit.Base.partial(MochiKit.Base.map, Clipperz.Async.collectResults("collectResults", {'_fullLabel':MochiKit.Base.methodcaller('fullLabel'), '_isArchived':MochiKit.Base.methodcaller('isArchived')}, {trace:false})), + Clipperz.Async.collectAll, + archivedCardsFilter, + MochiKit.Base.partial(MochiKit.Base.filter, this.regExpFilterGenerator(Clipperz.PM.DataModel.Record.regExpForNoTag(), '_fullLabel')), + function (someCards) { return someCards.length; }, + ], {trace:false}); + }, + + //---------------------------------------------------------------------------- + setPageProperties: function (aPageName, aKey, aValue) { var props = {}; props[aKey] = aValue; @@ -584,17 +616,19 @@ console.log("SET USER", aUser); renderTags: function () { return Clipperz.Async.callbacks("MainController.renderTags", [ - MochiKit.Base.method(this.user(), 'getTags'), -// MochiKit.Base.methodcaller('sort', Clipperz.Base.caseInsensitiveCompare), + MochiKit.Base.method(this.user(), 'getTags', this.shouldIncludeArchivedCards()), MochiKit.Base.method(this, 'setPageProperties', 'mainPage', 'tags'), + MochiKit.Base.method(this, 'getArchivedCardsCount'), + MochiKit.Base.method(this, 'setPageProperties', 'mainPage', 'archivedCardsCount'), + MochiKit.Base.method(this, 'getUntaggedCardsCount'), + MochiKit.Base.method(this, 'setPageProperties', 'mainPage', 'untaggedCardsCount'), + MochiKit.Base.method(this, 'setPageProperties', 'mainPage', 'shouldIncludeArchivedCards', this.shouldIncludeArchivedCards()), ], {trace:false}); }, renderAccountData: function () { return Clipperz.Async.callbacks("MainController.renderAccountData", [ MochiKit.Base.method(this, 'setFilter', 'ALL'), -// MochiKit.Base.method(this, 'refreshSelectedCards'), -// MochiKit.Base.method(this, 'renderTags'), MochiKit.Base.method(this, 'refreshUI', null) ], {trace:false}); }, @@ -823,10 +857,11 @@ console.log("SET USER", aUser); } else if (aPageName == 'registrationPage') { } else if (aPageName == 'mainPage') { extraProperties = { - 'messageBox': this.messageBoxContent(), - 'accountStatus': this.userAccountInfo(), - 'selectionPanelStatus': this.isSelectionPanelOpen() ? 'OPEN' : 'CLOSED', - 'settingsPanelStatus': this.isSettingsPanelOpen() ? 'OPEN' : 'CLOSED', + 'messageBox': this.messageBoxContent(), + 'accountStatus': this.userAccountInfo(), + 'selectionPanelStatus': this.isSelectionPanelOpen() ? 'OPEN' : 'CLOSED', + 'settingsPanelStatus': this.isSettingsPanelOpen() ? 'OPEN' : 'CLOSED', +// 'shouldIncludeArchivedCards': this.shouldIncludeArchivedCards(), // 'cards': …, // 'tags': …, // 'selectedCard': …, @@ -904,7 +939,7 @@ console.log("SET USER", aUser); }, //---------------------------------------------------------------------------- - +/* askConfirmation: function (aMessage) { var deferredResult; @@ -914,6 +949,17 @@ console.log("SET USER", aUser); return deferredResult; }, +*/ + + ask: function (someInfo) { + var deferredResult; + + deferredResult = new Clipperz.Async.Deferred('MainController.ask', {trace:false}); + + this.currentPage().setProps({'ask':someInfo, 'deferred':deferredResult}); + + return deferredResult; + }, //---------------------------------------------------------------------------- @@ -923,7 +969,14 @@ console.log("ADD CARD CLICK"); deleteCard_handler: function (anEvent) { return Clipperz.Async.callbacks("MainController.deleteCard_handler", [ - MochiKit.Base.method(this, 'askConfirmation', {'message':"Delete card?"}), +// MochiKit.Base.method(this, 'askConfirmation', {'message':"Delete card?"}), + MochiKit.Base.method(this, 'ask', { + 'question': "Delete card?", + 'possibleAnswers':{ + 'cancel': {'label':"No", 'isDefault':true, 'answer':MochiKit.Base.methodcaller('cancel')}, + 'delete': {'label':"Yes", 'isDefault':false, 'answer':MochiKit.Base.methodcaller('callback')} + } + }), MochiKit.Base.method(this.user(), 'getRecord', anEvent['reference']), MochiKit.Base.method(this.user(), 'deleteRecord'), MochiKit.Base.method(this.user(), 'saveChanges'), @@ -937,7 +990,7 @@ console.log("ADD CARD CLICK"); MochiKit.Base.methodcaller('archive'), MochiKit.Base.method(this.user(), 'saveChanges'), MochiKit.Base.method(this, 'refreshUI', anEvent['reference']) - ], {trace:true}); + ], {trace:false}); }, editCard_handler: function (anEvent) { @@ -966,16 +1019,21 @@ console.log("EDIT CARD", anEvent['reference']); return this.refreshSelectedCards(); }, + selectUntaggedCards_handler: function () { + this.setFilter('UNTAGGED'); + return this.refreshSelectedCards(); + }, + //............................................................................ showArchivedCards_handler: function () { this.setShouldIncludeArchivedCards(true); - return this.refreshSelectedCards(); + return this.refreshUI(); }, hideArchivedCards_handler: function () { this.setShouldIncludeArchivedCards(false); - return this.refreshSelectedCards(); + return this.refreshUI(); }, //---------------------------------------------------------------------------- diff --git a/frontend/delta/properties/delta.properties.json b/frontend/delta/properties/delta.properties.json index d803455..2a9ac38 100644 --- a/frontend/delta/properties/delta.properties.json +++ b/frontend/delta/properties/delta.properties.json @@ -39,7 +39,7 @@ "MochiKit/Selector.js", "-- MochiKit/Visual.js", - "React/react-with-addons-0.11.0.js", + "React/react-with-addons-0.11.1.js", "-- Hammer/hammer-1.0.5.js", "-- Cubiq/add2home.js", diff --git a/frontend/delta/scss/core/layout.scss b/frontend/delta/scss/core/layout.scss index 22b2dbb..df1cfc4 100644 --- a/frontend/delta/scss/core/layout.scss +++ b/frontend/delta/scss/core/layout.scss @@ -49,6 +49,8 @@ .showArchivedCards { @include flex(none); + + cursor: pointer; } }