FEATURE: Drafts view in user profile
* add drafts.json endpoint, user profile tab with drafts stream * improve drafts stream display in user profile * truncate excerpts in drafts list, better handling for resume draft action * improve draft stream SQL query, add rspec tests * if composer is open, quietly close it when user opens another draft from drafts stream; load PM draft only when user is in /u/username/messages (instead of /u/username) * cleanup * linting fixes * apply prettier styling to modified files * add client tests for drafts, includes a fixture for drafts.json * improvements to code following review * refresh drafts route when user deletes a draft open in the composer while being in the drafts route; minor prettier scss fix * added more spec tests, deleted an acceptance test for removing drafts that was too finicky, formatting and code style fixes, added appEvent for draft:destroyed * prettier, eslint fixes * use "username_lower" from users table, added error handling for rejected promises * adds guardian spec for can_see_drafts, adds improvements following code review * move DraftsController spec to its own file * fix failing drafts qunit test, use getOwner instead of deprecated this.container * limit test fixture for draft.json testing to new_topic request only
This commit is contained in:
@@ -2,6 +2,10 @@ import LoadMore from "discourse/mixins/load-more";
|
||||
import ClickTrack from "discourse/lib/click-track";
|
||||
import { selectedText } from "discourse/lib/utilities";
|
||||
import Post from "discourse/models/post";
|
||||
import DiscourseURL from "discourse/lib/url";
|
||||
import Draft from "discourse/models/draft";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { getOwner } from "discourse-common/lib/get-owner";
|
||||
|
||||
export default Ember.Component.extend(LoadMore, {
|
||||
loading: false,
|
||||
@@ -57,6 +61,41 @@ export default Ember.Component.extend(LoadMore, {
|
||||
});
|
||||
},
|
||||
|
||||
resumeDraft(item) {
|
||||
const composer = getOwner(this).lookup("controller:composer");
|
||||
if (composer.get("model.viewOpen")) {
|
||||
composer.close();
|
||||
}
|
||||
if (item.get("postUrl")) {
|
||||
DiscourseURL.routeTo(item.get("postUrl"));
|
||||
} else {
|
||||
Draft.get(item.draft_key)
|
||||
.then(d => {
|
||||
if (d.draft) {
|
||||
composer.open({
|
||||
draft: d.draft,
|
||||
draftKey: item.draft_key,
|
||||
draftSequence: d.draft_sequence
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
popupAjaxError(error);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
removeDraft(draft) {
|
||||
const stream = this.get("stream");
|
||||
Draft.clear(draft.draft_key, draft.sequence)
|
||||
.then(() => {
|
||||
stream.load(this.site);
|
||||
})
|
||||
.catch(error => {
|
||||
popupAjaxError(error);
|
||||
});
|
||||
},
|
||||
|
||||
loadMore() {
|
||||
if (this.get("loading")) {
|
||||
return;
|
||||
|
||||
@@ -849,7 +849,10 @@ export default Ember.Controller.extend({
|
||||
if (key === "new_topic") {
|
||||
this.send("clearTopicDraft");
|
||||
}
|
||||
Draft.clear(key, this.get("model.draftSequence"));
|
||||
|
||||
Draft.clear(key, this.get("model.draftSequence")).then(() => {
|
||||
this.appEvents.trigger("draft:destroyed", key);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -62,6 +62,11 @@ export default Ember.Controller.extend(CanCheckEmails, {
|
||||
return viewingSelf || isAdmin;
|
||||
},
|
||||
|
||||
@computed("viewingSelf", "currentUser.admin")
|
||||
showDrafts(viewingSelf, isAdmin) {
|
||||
return viewingSelf || isAdmin;
|
||||
},
|
||||
|
||||
@computed("viewingSelf", "currentUser.admin")
|
||||
showPrivateMessages(viewingSelf, isAdmin) {
|
||||
return (
|
||||
|
||||
@@ -12,6 +12,7 @@ export const CREATE_TOPIC = "createTopic",
|
||||
EDIT_SHARED_DRAFT = "editSharedDraft",
|
||||
PRIVATE_MESSAGE = "privateMessage",
|
||||
NEW_PRIVATE_MESSAGE_KEY = "new_private_message",
|
||||
NEW_TOPIC_KEY = "new_topic",
|
||||
REPLY = "reply",
|
||||
EDIT = "edit",
|
||||
REPLY_AS_NEW_TOPIC_KEY = "reply_as_new_topic",
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import RestModel from "discourse/models/rest";
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
import { postUrl } from "discourse/lib/utilities";
|
||||
import { userPath } from "discourse/lib/url";
|
||||
import User from "discourse/models/user";
|
||||
|
||||
import {
|
||||
NEW_TOPIC_KEY,
|
||||
NEW_PRIVATE_MESSAGE_KEY
|
||||
} from "discourse/models/composer";
|
||||
|
||||
export default RestModel.extend({
|
||||
@computed("draft_username")
|
||||
editableDraft(draftUsername) {
|
||||
return draftUsername === User.currentProp("username");
|
||||
},
|
||||
|
||||
@computed("username_lower")
|
||||
userUrl(usernameLower) {
|
||||
return userPath(usernameLower);
|
||||
},
|
||||
|
||||
@computed("topic_id")
|
||||
postUrl(topicId) {
|
||||
if (!topicId) return;
|
||||
|
||||
return postUrl(
|
||||
this.get("slug"),
|
||||
this.get("topic_id"),
|
||||
this.get("post_number")
|
||||
);
|
||||
},
|
||||
|
||||
@computed("draft_key", "post_number")
|
||||
draftType(draftKey, postNumber) {
|
||||
switch (draftKey) {
|
||||
case NEW_TOPIC_KEY:
|
||||
return I18n.t("drafts.new_topic");
|
||||
case NEW_PRIVATE_MESSAGE_KEY:
|
||||
return I18n.t("drafts.new_private_message");
|
||||
default:
|
||||
return postNumber
|
||||
? I18n.t("drafts.post_reply", { postNumber })
|
||||
: I18n.t("drafts.topic_reply");
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,105 @@
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { url } from "discourse/lib/computed";
|
||||
import RestModel from "discourse/models/rest";
|
||||
import UserDraft from "discourse/models/user-draft";
|
||||
import { emojiUnescape } from "discourse/lib/text";
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
|
||||
import {
|
||||
NEW_TOPIC_KEY,
|
||||
NEW_PRIVATE_MESSAGE_KEY
|
||||
} from "discourse/models/composer";
|
||||
|
||||
export default RestModel.extend({
|
||||
loaded: false,
|
||||
|
||||
init() {
|
||||
this._super();
|
||||
this.setProperties({
|
||||
itemsLoaded: 0,
|
||||
content: [],
|
||||
lastLoadedUrl: null
|
||||
});
|
||||
},
|
||||
|
||||
baseUrl: url(
|
||||
"itemsLoaded",
|
||||
"user.username_lower",
|
||||
"/drafts.json?offset=%@&username=%@"
|
||||
),
|
||||
|
||||
load(site) {
|
||||
this.setProperties({
|
||||
itemsLoaded: 0,
|
||||
content: [],
|
||||
lastLoadedUrl: null,
|
||||
site: site
|
||||
});
|
||||
return this.findItems();
|
||||
},
|
||||
|
||||
@computed("content.length", "loaded")
|
||||
noContent(contentLength, loaded) {
|
||||
return loaded && contentLength === 0;
|
||||
},
|
||||
|
||||
remove(draft) {
|
||||
let content = this.get("content").filter(
|
||||
item => item.sequence !== draft.sequence
|
||||
);
|
||||
this.setProperties({ content, itemsLoaded: content.length });
|
||||
},
|
||||
|
||||
findItems() {
|
||||
let findUrl = this.get("baseUrl");
|
||||
|
||||
const lastLoadedUrl = this.get("lastLoadedUrl");
|
||||
if (lastLoadedUrl === findUrl) {
|
||||
return Ember.RSVP.resolve();
|
||||
}
|
||||
|
||||
if (this.get("loading")) {
|
||||
return Ember.RSVP.resolve();
|
||||
}
|
||||
|
||||
this.set("loading", true);
|
||||
|
||||
return ajax(findUrl, { cache: "false" })
|
||||
.then(result => {
|
||||
if (result && result.no_results_help) {
|
||||
this.set("noContentHelp", result.no_results_help);
|
||||
}
|
||||
if (result && result.drafts) {
|
||||
const copy = Em.A();
|
||||
result.drafts.forEach(draft => {
|
||||
let draftData = JSON.parse(draft.data);
|
||||
draft.post_number = draftData.postId || null;
|
||||
if (
|
||||
draft.draft_key === NEW_PRIVATE_MESSAGE_KEY ||
|
||||
draft.draft_key === NEW_TOPIC_KEY
|
||||
) {
|
||||
draft.title = draftData.title;
|
||||
}
|
||||
draft.title = emojiUnescape(
|
||||
Handlebars.Utils.escapeExpression(draft.title)
|
||||
);
|
||||
if (draft.category_id) {
|
||||
draft.category =
|
||||
this.site.categories.findBy("id", draft.category_id) || null;
|
||||
}
|
||||
|
||||
copy.pushObject(UserDraft.create(draft));
|
||||
});
|
||||
this.get("content").pushObjects(copy);
|
||||
this.setProperties({
|
||||
loaded: true,
|
||||
itemsLoaded: this.get("itemsLoaded") + result.drafts.length
|
||||
});
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
this.set("loading", false);
|
||||
this.set("lastLoadedUrl", findUrl);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -13,6 +13,7 @@ import Badge from "discourse/models/badge";
|
||||
import UserBadge from "discourse/models/user-badge";
|
||||
import UserActionStat from "discourse/models/user-action-stat";
|
||||
import UserAction from "discourse/models/user-action";
|
||||
import UserDraftsStream from "discourse/models/user-drafts-stream";
|
||||
import Group from "discourse/models/group";
|
||||
import { emojiUnescape } from "discourse/lib/text";
|
||||
import PreloadStore from "preload-store";
|
||||
@@ -47,6 +48,11 @@ const User = RestModel.extend({
|
||||
return UserPostsStream.create({ user: this });
|
||||
},
|
||||
|
||||
@computed()
|
||||
userDraftsStream() {
|
||||
return UserDraftsStream.create({ user: this });
|
||||
},
|
||||
|
||||
staff: Em.computed.or("admin", "moderator"),
|
||||
|
||||
destroySession() {
|
||||
|
||||
@@ -112,6 +112,7 @@ export default function() {
|
||||
this.route("likesGiven", { path: "likes-given" });
|
||||
this.route("bookmarks");
|
||||
this.route("pending");
|
||||
this.route("drafts");
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
export default Discourse.Route.extend({
|
||||
model() {
|
||||
let userDraftsStream = this.modelFor("user").get("userDraftsStream");
|
||||
return userDraftsStream.load(this.site).then(() => userDraftsStream);
|
||||
},
|
||||
|
||||
renderTemplate() {
|
||||
this.render("user_stream");
|
||||
},
|
||||
|
||||
setupController(controller, model) {
|
||||
controller.set("model", model);
|
||||
this.appEvents.on("draft:destroyed", this, this.refresh);
|
||||
},
|
||||
|
||||
actions: {
|
||||
didTransition() {
|
||||
this.controllerFor("user-activity")._showFooter();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,3 +1,5 @@
|
||||
import Draft from "discourse/models/draft";
|
||||
|
||||
export default Discourse.Route.extend({
|
||||
renderTemplate() {
|
||||
this.render("user/messages");
|
||||
@@ -7,6 +9,23 @@ export default Discourse.Route.extend({
|
||||
return this.modelFor("user");
|
||||
},
|
||||
|
||||
setupController(controller, user) {
|
||||
const composerController = this.controllerFor("composer");
|
||||
controller.set("model", user);
|
||||
if (this.currentUser) {
|
||||
Draft.get("new_private_message").then(data => {
|
||||
if (data.draft) {
|
||||
composerController.open({
|
||||
draft: data.draft,
|
||||
draftKey: "new_private_message",
|
||||
ignoreIfChanged: true,
|
||||
draftSequence: data.draft_sequence
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
willTransition: function() {
|
||||
this._super();
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import Draft from "discourse/models/draft";
|
||||
|
||||
export default Discourse.Route.extend({
|
||||
titleToken() {
|
||||
const username = this.modelFor("user").get("username");
|
||||
@@ -67,21 +65,6 @@ export default Discourse.Route.extend({
|
||||
setupController(controller, user) {
|
||||
controller.set("model", user);
|
||||
this.searchService.set("searchContext", user.get("searchContext"));
|
||||
|
||||
const composerController = this.controllerFor("composer");
|
||||
controller.set("model", user);
|
||||
if (this.currentUser) {
|
||||
Draft.get("new_private_message").then(function(data) {
|
||||
if (data.draft) {
|
||||
composerController.open({
|
||||
draft: data.draft,
|
||||
draftKey: "new_private_message",
|
||||
ignoreIfChanged: true,
|
||||
draftSequence: data.draft_sequence
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
activate() {
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
<div class='clearfix info'>
|
||||
<a href={{item.userUrl}} data-user-card={{item.username}} class='avatar-link'><div class='avatar-wrapper'>{{avatar item imageSize="large" extraClasses="actor" ignoreTitle="true"}}</div></a>
|
||||
<span class='time'>{{format-date item.created_at}}</span>
|
||||
{{expand-post item=item}}
|
||||
{{#if item.draftType}}
|
||||
<span class='draft-type'>{{{item.draftType}}}</span>
|
||||
{{else}}
|
||||
{{expand-post item=item}}
|
||||
{{/if}}
|
||||
<div class='stream-topic-details'>
|
||||
<div class='stream-topic-title'>
|
||||
{{topic-status topic=item disableActions=true}}
|
||||
<span class="title">
|
||||
<a href={{item.postUrl}}>{{{item.title}}}</a>
|
||||
{{#if item.postUrl}}
|
||||
<a href={{item.postUrl}}>{{{item.title}}}</a>
|
||||
{{else}}
|
||||
{{{item.title}}}
|
||||
{{/if}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="category">{{category-link item.category}}</div>
|
||||
@@ -50,3 +58,10 @@
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/each}}
|
||||
|
||||
{{#if item.editableDraft}}
|
||||
<div class='user-stream-item-draft-actions'>
|
||||
{{d-button action=resumeDraft actionParam=item icon="pencil" label='drafts.resume' class="resume-draft"}}
|
||||
{{d-button action=removeDraft actionParam=item icon="times" label='drafts.remove' class="remove-draft"}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
{{#each stream.content as |item|}}
|
||||
{{user-stream-item item=item removeBookmark=(action "removeBookmark")}}
|
||||
{{user-stream-item
|
||||
item=item
|
||||
removeBookmark=(action "removeBookmark")
|
||||
resumeDraft=(action "resumeDraft")
|
||||
removeDraft=(action "removeDraft")
|
||||
}}
|
||||
{{/each}}
|
||||
|
||||
@@ -9,6 +9,11 @@
|
||||
<li>
|
||||
{{#link-to 'userActivity.replies'}}{{i18n 'user_action_groups.5'}}{{/link-to}}
|
||||
</li>
|
||||
{{#if user.showDrafts}}
|
||||
<li>
|
||||
{{#link-to 'userActivity.drafts'}}{{i18n 'user_action_groups.15'}}{{/link-to}}
|
||||
</li>
|
||||
{{/if}}
|
||||
<li>
|
||||
{{#link-to 'userActivity.likesGiven'}}{{i18n 'user_action_groups.1'}}{{/link-to}}
|
||||
</li>
|
||||
|
||||
Reference in New Issue
Block a user