Commit c065e19d authored by Steffen van Bergerem's avatar Steffen van Bergerem

Merge pull request #6864 from cmrd-senya/6120-aspects-creation-ui

Refactor  aspects membership dropdown to use handlebars template
parents 2f80ab8f e7d8de29
......@@ -114,6 +114,7 @@ before.
* Moved the entire federation implementation into its own gem. 🎉 [#6873](https://github.com/diaspora/diaspora/pull/6873)
* Remove `StatusMessage#raw_message` [#6921](https://github.com/diaspora/diaspora/pull/6921)
* Extract photo export into a service class [#6922](https://github.com/diaspora/diaspora/pull/6922)
* Use handlebars template for aspect membership dropdown [#6864](https://github.com/diaspora/diaspora/pull/6864)
## Bug fixes
* Destroy Participation when removing interactions with a post [#5852](https://github.com/diaspora/diaspora/pull/5852)
......@@ -136,6 +137,7 @@ before.
* Connection tester handles invalid NodeInfo implementations [#6890](https://github.com/diaspora/diaspora/pull/6890)
* Do not allow to change email to an already used one [#6905](https://github.com/diaspora/diaspora/pull/6905)
* Correctly filter mentions on the server side [#6902](https://github.com/diaspora/diaspora/pull/6902)
* Add aspects to the aspect membership dropdown when creating them on the getting started page [#6864](https://github.com/diaspora/diaspora/pull/6864)
## Features
* Support color themes [#6033](https://github.com/diaspora/diaspora/pull/6033)
......
......@@ -120,9 +120,6 @@ var app = {
setupGlobalViews: function() {
app.hovercard = new app.views.Hovercard();
$('.aspect_membership_dropdown').each(function(){
new app.views.AspectMembership({el: this});
});
app.sidebar = new app.views.Sidebar();
app.backToTop = new app.views.BackToTop({el: $(document)});
app.flashMessages = new app.views.FlashMessages({el: $("#flash-container")});
......
// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later
app.collections.AspectMemberships = Backbone.Collection.extend({
model: app.models.AspectMembership
model: app.models.AspectMembership,
findByAspectId: function(id) {
return this.find(function(membership) { return membership.belongsToAspect(id); });
}
});
// @license-end
// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later
app.collections.AspectSelections = Backbone.Collection.extend({
model: app.models.AspectSelection,
selectedGetAttribute: function(attribute) {
return _.pluck(_.filter(this.toJSON(), function(a) {
return a.selected;
}), attribute);
},
allSelected: function() {
return this.length === _.filter(this.toJSON(), function(a) { return a.selected; }).length;
},
selectAll: function() {
this.map(function(a) { a.set({"selected": true}); });
},
deselectAll: function() {
this.map(function(a) { a.set({"selected": false}); });
},
toSentence: function() {
var separator = Diaspora.I18n.t("comma") + " ";
var pattern = new RegExp(Diaspora.I18n.t("comma") + "\\s([^" + Diaspora.I18n.t("comma") + "]+)$");
return this.selectedGetAttribute("name").join(separator).replace(pattern, " " + Diaspora.I18n.t("and") + " $1")
|| Diaspora.I18n.t("my_aspects");
}
});
// @license-end
// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later
app.collections.Aspects = Backbone.Collection.extend({
model: app.models.AspectSelection,
selectedAspects: function(attribute){
return _.pluck(_.filter(this.toJSON(), function(a){
return a.selected;
}), attribute);
},
allSelected: function(){
return this.length === _.filter(this.toJSON(), function(a){ return a.selected; }).length;
},
selectAll: function(){
this.map(function(a){ a.set({ 'selected' : true })} );
},
deselectAll: function(){
this.map(function(a){ a.set({ 'selected' : false })} );
},
toSentence: function(){
var separator = Diaspora.I18n.t("comma") + ' ';
return this.selectedAspects('name').join(separator).replace(/,\s([^,]+)$/, ' ' + Diaspora.I18n.t("and") + ' $1') || Diaspora.I18n.t("my_aspects");
}
model: app.models.Aspect,
url: "/aspects"
});
// @license-end
......@@ -5,6 +5,11 @@
* (only valid for the context of the current user)
*/
app.models.AspectMembership = Backbone.Model.extend({
urlRoot: "/aspect_memberships"
urlRoot: "/aspect_memberships",
belongsToAspect: function(aspectId) {
var aspect = this.get("aspect");
return aspect && aspect.id === aspectId;
}
});
// @license-end
......@@ -3,11 +3,14 @@
app.models.Contact = Backbone.Model.extend({
initialize : function() {
this.aspectMemberships = new app.collections.AspectMemberships(this.get("aspect_memberships"));
if(this.get("person")) { this.person = new app.models.Person(this.get("person")); }
if (this.get("person")) {
this.person = new app.models.Person(this.get("person"));
this.person.contact = this;
}
},
inAspect : function(id) {
return this.aspectMemberships.any(function(membership){ return membership.get("aspect").id === id; });
return this.aspectMemberships.any(function(membership) { return membership.belongsToAspect(id); });
}
});
// @license-end
......@@ -6,8 +6,13 @@ app.models.Person = Backbone.Model.extend({
},
initialize: function() {
if( this.get('profile') )
this.profile = new app.models.Profile(this.get('profile'));
if (this.get("profile")) {
this.profile = new app.models.Profile(this.get("profile"));
}
if (this.get("contact")) {
this.contact = new app.models.Contact(this.get("contact"));
this.contact.person = this;
}
},
isSharing: function() {
......
......@@ -26,8 +26,8 @@ app.models.StreamAspects = app.models.Stream.extend({
fetchDone: function() {
this.triggerFetchedEvents();
if (app.aspects) {
app.aspects.trigger('aspectStreamFetched');
if (app.aspectSelections) {
app.aspectSelections.trigger("aspectStreamFetched");
}
}
});
......
// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later
app.pages.GettingStarted = app.views.Base.extend({
el: "#hello-there",
templateName: false,
subviews: {
".aspect_membership_dropdown": "aspectMembershipView"
},
initialize: function(opts) {
this.inviter = opts.inviter;
app.events.on("aspect:create", this.render, this);
},
aspectMembershipView: function() {
return new app.views.AspectMembership({person: this.inviter, dropdownMayCreateNewAspect: true});
}
});
// @license-end
......@@ -21,11 +21,11 @@ app.pages.Profile = app.views.Base.extend({
this._populateModel(opts);
}
if(app.hasPreload("photos")){
this.photos = app.parsePreload("photos");
if (app.hasPreload("photos_count")) {
this.photos = app.parsePreload("photos_count");
}
if(app.hasPreload("contacts")){
this.contacts = app.parsePreload("contacts");
if (app.hasPreload("contacts_count")) {
this.contacts = app.parsePreload("contacts_count");
}
this.streamCollection = _.has(opts, "streamCollection") ? opts.streamCollection : null;
......@@ -40,6 +40,8 @@ app.pages.Profile = app.views.Base.extend({
app.events.on("person:unblock:"+id, this.reload, this);
app.events.on("aspect:create", this.reload, this);
app.events.on("aspect_membership:update", this.reload, this);
app.router.renderAspectMembershipDropdowns(this.$el);
},
_populateModel: function(opts) {
......
......@@ -5,6 +5,7 @@ app.Router = Backbone.Router.extend({
"help/:section": "help",
"help/": "help",
"help": "help",
"getting_started": "gettingStarted",
"contacts": "contacts",
"conversations": "conversations",
"user/edit": "settings",
......@@ -27,6 +28,8 @@ app.Router = Backbone.Router.extend({
"tags/:name": "followed_tags",
"people/:id/photos": "photos",
"people/:id/contacts": "profile",
"people": "pageWithAspectMembershipDropdowns",
"notifications": "pageWithAspectMembershipDropdowns",
"people/:id": "profile",
"u/:name": "profile"
......@@ -59,7 +62,7 @@ app.Router = Backbone.Router.extend({
contacts: function() {
app.aspect = new app.models.Aspect(gon.preloads.aspect);
app.contacts = new app.collections.Contacts(app.parsePreload("contacts"));
this._loadRelationshipsPreloads();
var stream = new app.views.ContactStream({
collection: app.contacts,
......@@ -69,6 +72,13 @@ app.Router = Backbone.Router.extend({
app.page = new app.pages.Contacts({stream: stream});
},
gettingStarted: function() {
this._loadAspects();
this.renderPage(function() {
return new app.pages.GettingStarted({inviter: new app.models.Person(app.parsePreload("inviter"))});
});
},
conversations: function() {
app.conversations = new app.views.Conversations();
},
......@@ -97,6 +107,7 @@ app.Router = Backbone.Router.extend({
},
stream : function() {
this._loadAspects();
app.stream = new app.models.Stream();
app.stream.fetch();
this._initializeStreamView();
......@@ -134,14 +145,16 @@ app.Router = Backbone.Router.extend({
},
aspects: function() {
app.aspects = app.aspects || new app.collections.Aspects(app.currentUser.get("aspects"));
this.aspectsList = this.aspectsList || new app.views.AspectsList({ collection: app.aspects });
this._loadAspects();
app.aspectSelections = app.aspectSelections ||
new app.collections.AspectSelections(app.currentUser.get("aspects"));
this.aspectsList = this.aspectsList || new app.views.AspectsList({collection: app.aspectSelections});
this.aspectsList.render();
this.aspects_stream();
},
aspects_stream : function(){
var ids = app.aspects.selectedAspects("id");
var ids = app.aspectSelections.selectedGetAttribute("id");
app.stream = new app.models.StreamAspects([], { aspects_ids: ids });
app.stream.fetch();
this._initializeStreamView();
......@@ -156,6 +169,7 @@ app.Router = Backbone.Router.extend({
},
profile: function() {
this._loadRelationshipsPreloads();
this.renderPage(function() {
return new app.pages.Profile({
el: $("body > #profile_container")
......@@ -163,6 +177,32 @@ app.Router = Backbone.Router.extend({
});
},
pageWithAspectMembershipDropdowns: function() {
this._loadRelationshipsPreloads();
this.renderAspectMembershipDropdowns($(document));
},
_loadAspects: function() {
app.aspects = new app.collections.Aspects(app.currentUser.get("aspects"));
},
_loadContacts: function() {
app.contacts = new app.collections.Contacts(app.parsePreload("contacts"));
},
_loadRelationshipsPreloads: function() {
this._loadContacts();
this._loadAspects();
},
renderAspectMembershipDropdowns: function($context) {
$context.find(".aspect_membership_dropdown.placeholder").each(function() {
var personId = $(this).data("personId");
var view = new app.views.AspectMembership({person: app.contacts.findWhere({"person_id": personId}).person});
$(this).html(view.render().$el);
});
},
_hideInactiveStreamLists: function() {
if(this.aspectsList && Backbone.history.fragment !== "aspects") {
this.aspectsList.hideAspectsList();
......
......@@ -73,7 +73,8 @@ app.views.Base = Backbone.View.extend({
var self = this;
_.each(this.subviews, function(property, selector){
var view = _.isFunction(self[property]) ? self[property]() : self[property];
if(view) {
if (view && self.$(selector).length > 0) {
self.$(selector).empty();
self.$(selector).html(view.render().el);
view.delegateEvents();
}
......
......@@ -9,20 +9,18 @@ app.views.AspectCreate = app.views.Base.extend({
},
initialize: function(opts) {
this._personId = _.has(opts, "personId") ? opts.personId : null;
if (opts && opts.person) {
this.person = opts.person;
this._personId = opts.person.id;
}
},
presenter: function() {
return _.extend(this.defaultPresenter(), {
addPersonId: this._personId !== null,
personId : this._personId
});
},
postRenderTemplate: function() {
this.modal = this.$(".modal");
},
_contactsVisible: function() {
return this.$("#aspect_contacts_visible").is(":checked");
},
......@@ -38,28 +36,52 @@ app.views.AspectCreate = app.views.Base.extend({
}
},
createAspect: function() {
var aspect = new app.models.Aspect({
"person_id": this._personId,
"name": this._name(),
"contacts_visible": this._contactsVisible()
postRenderTemplate: function() {
this.$(".modal").on("hidden.bs.modal", null, this, function(e) {
e.data.ensureEventsOrder();
});
},
createAspect: function() {
this._eventsCounter = 0;
var self = this;
aspect.on("sync", function(response) {
var aspectId = response.get("id"),
aspectName = response.get("name");
this.$(".modal").modal("hide");
self.modal.modal("hide");
app.events.trigger("aspect:create", aspectId);
this.listenToOnce(app.aspects, "sync", function(response) {
var aspectName = response.get("name"),
membership = response.get("aspect_membership");
this._newAspectId = response.get("id");
if (membership) {
if (!this.person.contact) {
this.person.contact = new app.models.Contact();
}
this.person.contact.aspectMemberships.add([membership]);
}
this.ensureEventsOrder();
app.flashMessages.success(Diaspora.I18n.t("aspects.create.success", {"name": aspectName}));
});
aspect.on("error", function() {
self.modal.modal("hide");
this.listenToOnce(app.aspects, "error", function() {
app.flashMessages.error(Diaspora.I18n.t("aspects.create.failure"));
this.stopListening(app.aspects, "sync");
});
app.aspects.create({
"person_id": this._personId || null,
"name": this._name(),
"contacts_visible": this._contactsVisible()
});
return aspect.save();
},
// ensure that we trigger the aspect:create event only after both hidden.bs.modal and and aspects sync happens
ensureEventsOrder: function() {
this._eventsCounter++;
if (this._eventsCounter > 1) {
app.events.trigger("aspect:create", this._newAspectId);
}
}
});
// @license-end
// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later
//= require ./aspects_dropdown_view
/**
* this view lets the user (de-)select aspect memberships in the context
* of another users profile or the contact page.
......@@ -9,7 +7,13 @@
* updates to the list of aspects are immediately propagated to the server, and
* the results are dislpayed as flash messages.
*/
app.views.AspectMembership = app.views.AspectsDropdown.extend({
app.views.AspectMembership = app.views.Base.extend({
templateName: "aspect_membership_dropdown",
className: "btn-group aspect_dropdown aspect_membership_dropdown",
subviews: {
".newAspectContainer": "aspectCreateView"
},
events: {
"click ul.aspect_membership.dropdown-menu > li.aspect_selector"
......@@ -18,70 +22,89 @@ app.views.AspectMembership = app.views.AspectsDropdown.extend({
: "_clickHandler"
},
initialize: function() {
initialize: function(opts) {
_.extend(this, opts);
this.list_item = null;
this.dropdown = null;
if (this.$(".newAspectContainer").length > 0) {
this.aspectCreateView = new app.views.AspectCreate({
el: this.$(".newAspectContainer"),
personId: this.$("ul.dropdown-menu").data("person_id")
});
this.aspectCreateView.render();
}
},
presenter: function() {
var aspectMembershipsLength = this.person.contact ? this.person.contact.aspectMemberships.length : 0;
return _.extend(this.defaultPresenter(), {
aspects: this.aspectsPresenter(),
dropdownMayCreateNewAspect: this.dropdownMayCreateNewAspect
}, aspectMembershipsLength === 0 ? {
extraButtonClass: "btn-default",
noAspectIsSelected: true
} : { // this.contact.aspectMemberships.length > 0
aspectMembershipsLength: aspectMembershipsLength,
allAspectsAreSelected: aspectMembershipsLength === app.aspects.length,
onlyOneAspectIsSelected: aspectMembershipsLength === 1,
firstMembershipName: this.person.contact.aspectMemberships.at(0).get("aspect").name,
extraButtonClass: "btn-success"
});
},
aspectsPresenter: function() {
return _.map(app.aspects.models, function(aspect) {
return _.extend(
this.person.contact ?
{membership: this.person.contact.aspectMemberships.findByAspectId(aspect.attributes.id)} : {},
aspect.attributes // id, name
);
}, this);
},
aspectCreateView: function() {
return new app.views.AspectCreate({
person: this.person
});
},
// decide what to do when clicked
// -> addMembership
// -> removeMembership
_clickHandler: function(evt) {
var promise = null;
this.list_item = $(evt.target).closest('li.aspect_selector');
this.dropdown = this.list_item.parent();
this.list_item.addClass('loading');
if( this.list_item.is('.selected') ) {
var membership_id = this.list_item.data('membership_id');
promise = this.removeMembership(membership_id);
if (this.list_item.is(".selected")) {
this.removeMembership(this.list_item.data("membership_id"));
} else {
var aspect_id = this.list_item.data('aspect_id');
var person_id = this.dropdown.data('person_id');
promise = this.addMembership(person_id, aspect_id);
this.addMembership(this.list_item.data("aspect_id"));
}
promise && promise.always(function() {
// trigger a global event
app.events.trigger('aspect_membership:update');
});
return false; // stop the event
},
// return the (short) name of the person associated with the current dropdown
_name: function() {
return this.dropdown.data('person-short-name');
return this.person.name || this.person.get("name");
},
_personId: function() {
return this.person.id;
},
// create a membership for the given person in the given aspect
addMembership: function(person_id, aspect_id) {
var aspect_membership = new app.models.AspectMembership({
'person_id': person_id,
'aspect_id': aspect_id
});
addMembership: function(aspectId) {
if (!this.person.contact) {
this.person.contact = new app.models.Contact();
}
aspect_membership.on('sync', this._successSaveCb, this);
aspect_membership.on('error', function() {
this.listenToOnce(this.person.contact.aspectMemberships, "sync", this._successSaveCb);
this.listenToOnce(this.person.contact.aspectMemberships, "error", function() {
this._displayError('aspect_dropdown.error');
}, this);
});
return aspect_membership.save();
return this.person.contact.aspectMemberships.create({"aspect_id": aspectId, "person_id": this._personId()});
},
_successSaveCb: function(aspectMembership) {
var aspectId = aspectMembership.get("aspect_id"),
membershipId = aspectMembership.get("id"),
li = this.dropdown.find("li[data-aspect_id='" + aspectId + "']"),
personId = li.closest("ul.dropdown-menu").data("person_id"),
startSharing = false;
// the user didn't have this person in any aspects before, congratulate them
......@@ -93,15 +116,11 @@ app.views.AspectMembership = app.views.AspectsDropdown.extend({
}
app.events.trigger("aspect_membership:create", {
membership: { aspectId: aspectId, personId: personId },
membership: {aspectId: aspectId, personId: this._personId()},
startSharing: startSharing
});
li.attr("data-membership_id", membershipId) // just to be sure...
.data("membership_id", membershipId);
this.updateSummary(li);
this._done();
this.render();
app.events.trigger("aspect_membership:update");
},
// show an error flash msg
......@@ -114,44 +133,35 @@ app.views.AspectMembership = app.views.AspectsDropdown.extend({
},
// remove the membership with the given id
removeMembership: function(membership_id) {
var aspect_membership = new app.models.AspectMembership({
'id': membership_id
removeMembership: function(membershipId) {
var membership = this.person.contact.aspectMemberships.get(membershipId);
this.listenToOnce(membership, "sync", this._successDestroyCb);
this.listenToOnce(membership, "error", function() {
this._displayError("aspect_dropdown.error_remove");
});
aspect_membership.on('sync', this._successDestroyCb, this);
aspect_membership.on('error', function() {
this._displayError('aspect_dropdown.error_remove');
}, this);
return aspect_membership.destroy();
return membership.destroy();
},
_successDestroyCb: function(aspectMembership) {
var membershipId = aspectMembership.get("id"),
li = this.dropdown.find("li[data-membership_id='" + membershipId + "']"),
aspectId = li.data("aspect_id"),
personId = li.closest("ul.dropdown-menu").data("person_id"),
aspectId = aspectMembership.get("aspect").id,
stopSharing = false;
li.removeAttr("data-membership_id")
.removeData("membership_id");
this.updateSummary(li);
this.render();
// we just removed the last aspect, inform the user with a flash message
// that he is no longer sharing with that person
if( this.dropdown.find("li.selected").length === 0 ) {
if (this.$el.find("li.selected").length === 0) {
var msg = Diaspora.I18n.t("aspect_dropdown.stopped_sharing_with", { "name": this._name() });
stopSharing = true;
app.flashMessages.success(msg);
}
app.events.trigger("aspect_membership:destroy", {
membership: { aspectId: aspectId, personId: personId },
membership: {aspectId: aspectId, personId: this._personId()},
stopSharing: stopSharing
});
this._done();
app.events.trigger("aspect_membership:update");
},
// cleanup tasks after aspect selection
......@@ -160,11 +170,5 @@ app.views.AspectMembership = app.views.AspectsDropdown.extend({
this.list_item.removeClass('loading');
}
},
// refresh the button text to reflect the current aspect selection status
updateSummary: function(target) {
this._toggleCheckbox(target);
this._updateButton("btn-success");
}
});
// @license-end