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:
parent
70ea153dce
commit
1f45215537
|
@ -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>
|
||||
|
|
|
@ -36,13 +36,18 @@
|
|||
}
|
||||
|
||||
.time,
|
||||
.delete-info {
|
||||
.delete-info,
|
||||
.draft-type {
|
||||
display: block;
|
||||
float: right;
|
||||
color: lighten($primary, 40%);
|
||||
font-size: $font-down-2;
|
||||
}
|
||||
|
||||
.draft-type {
|
||||
clear: right;
|
||||
}
|
||||
|
||||
.delete-info i {
|
||||
font-size: $font-0;
|
||||
}
|
||||
|
@ -83,7 +88,8 @@
|
|||
padding: 3px 5px 5px 5px;
|
||||
}
|
||||
|
||||
.remove-bookmark {
|
||||
.remove-bookmark,
|
||||
.remove-draft {
|
||||
float: right;
|
||||
margin-top: -4px;
|
||||
}
|
||||
|
@ -95,6 +101,7 @@
|
|||
|
||||
p {
|
||||
display: inline-block;
|
||||
|
||||
span {
|
||||
color: $primary;
|
||||
}
|
||||
|
@ -131,7 +138,8 @@
|
|||
}
|
||||
|
||||
.user-stream .child-actions, /* DEPRECATED: '.user-stream .child-actions' selector*/
|
||||
.user-stream-item-actions {
|
||||
.user-stream-item-actions,
|
||||
.user-stream-item-draft-actions {
|
||||
margin-top: 8px;
|
||||
|
||||
.avatar-link {
|
||||
|
|
|
@ -11,7 +11,8 @@
|
|||
}
|
||||
|
||||
.time,
|
||||
.delete-info {
|
||||
.delete-info,
|
||||
.draft-type {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
class DraftsController < ApplicationController
|
||||
requires_login
|
||||
|
||||
skip_before_action :check_xhr, :preload_json
|
||||
|
||||
def index
|
||||
params.require(:username)
|
||||
params.permit(:offset)
|
||||
params.permit(:limit)
|
||||
|
||||
user = fetch_user_from_params
|
||||
|
||||
opts = {
|
||||
user: user,
|
||||
offset: params[:offset],
|
||||
limit: params[:limit]
|
||||
}
|
||||
|
||||
guardian.ensure_can_see_drafts!(user)
|
||||
stream = Draft.stream(opts)
|
||||
stream.each do |d|
|
||||
parsed_data = JSON.parse(d.data)
|
||||
if parsed_data
|
||||
if parsed_data['reply']
|
||||
d.raw = parsed_data['reply']
|
||||
end
|
||||
if parsed_data['categoryId'].present? && !d.category_id.present?
|
||||
d.category_id = parsed_data['categoryId']
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
help_key = "user_activity.no_drafts"
|
||||
if user == current_user
|
||||
help_key += ".self"
|
||||
else
|
||||
help_key += ".others"
|
||||
end
|
||||
|
||||
render json: {
|
||||
drafts: serialize_data(stream, DraftSerializer),
|
||||
no_results_help: I18n.t(help_key)
|
||||
}
|
||||
|
||||
end
|
||||
|
||||
end
|
|
@ -43,6 +43,43 @@ class Draft < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
def self.stream(opts = nil)
|
||||
opts ||= {}
|
||||
|
||||
user_id = opts[:user].id
|
||||
offset = (opts[:offset] || 0).to_i
|
||||
limit = (opts[:limit] || 30).to_i
|
||||
|
||||
# JOIN of topics table based on manipulating draft_key seems imperfect
|
||||
builder = DB.build <<~SQL
|
||||
SELECT
|
||||
d.*, t.title, t.id topic_id, t.archetype,
|
||||
t.category_id, t.closed topic_closed, t.archived topic_archived,
|
||||
pu.username, pu.name, pu.id user_id, pu.uploaded_avatar_id, pu.username_lower,
|
||||
du.username draft_username, NULL as raw, NULL as cooked, NULL as post_number
|
||||
FROM drafts d
|
||||
LEFT JOIN topics t ON
|
||||
CASE
|
||||
WHEN d.draft_key LIKE '%' || '#{EXISTING_TOPIC}' || '%'
|
||||
THEN CAST(replace(d.draft_key, '#{EXISTING_TOPIC}', '') AS INT)
|
||||
ELSE 0
|
||||
END = t.id
|
||||
JOIN users pu on pu.id = COALESCE(t.user_id, d.user_id)
|
||||
JOIN users du on du.id = #{user_id}
|
||||
/*where*/
|
||||
/*order_by*/
|
||||
/*offset*/
|
||||
/*limit*/
|
||||
SQL
|
||||
|
||||
builder
|
||||
.where('d.user_id = :user_id', user_id: user_id.to_i)
|
||||
.order_by('d.updated_at desc')
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
.query
|
||||
end
|
||||
|
||||
def self.cleanup!
|
||||
DB.exec("DELETE FROM drafts where sequence < (
|
||||
SELECT max(s.sequence) from draft_sequences s
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
require_relative 'post_item_excerpt'
|
||||
|
||||
class DraftSerializer < ApplicationSerializer
|
||||
include PostItemExcerpt
|
||||
|
||||
attributes :created_at,
|
||||
:draft_key,
|
||||
:sequence,
|
||||
:draft_username,
|
||||
:avatar_template,
|
||||
:data,
|
||||
:topic_id,
|
||||
:username,
|
||||
:username_lower,
|
||||
:name,
|
||||
:user_id,
|
||||
:title,
|
||||
:slug,
|
||||
:category_id,
|
||||
:closed,
|
||||
:archetype,
|
||||
:archived
|
||||
|
||||
def avatar_template
|
||||
User.avatar_template(object.username, object.uploaded_avatar_id)
|
||||
end
|
||||
|
||||
def slug
|
||||
Slug.for(object.title)
|
||||
end
|
||||
|
||||
def include_slug?
|
||||
object.title.present?
|
||||
end
|
||||
|
||||
def closed
|
||||
object.topic_closed
|
||||
end
|
||||
|
||||
def archived
|
||||
object.topic_archived
|
||||
end
|
||||
|
||||
def include_closed?
|
||||
object.topic_closed.present?
|
||||
end
|
||||
|
||||
def include_archived?
|
||||
object.topic_archived.present?
|
||||
end
|
||||
|
||||
def include_category_id?
|
||||
object.category_id.present?
|
||||
end
|
||||
|
||||
end
|
|
@ -287,6 +287,14 @@ en:
|
|||
remove: "Remove Bookmark"
|
||||
confirm_clear: "Are you sure you want to clear all the bookmarks from this topic?"
|
||||
|
||||
drafts:
|
||||
resume: "Resume Draft"
|
||||
remove: "Remove Draft"
|
||||
new_topic: "New topic draft"
|
||||
new_private_message: "New private message draft"
|
||||
topic_reply: "Draft reply"
|
||||
post_reply: "Draft reply to #{{postNumber}}"
|
||||
|
||||
topic_count_latest:
|
||||
one: "See {{count}} new or updated topic"
|
||||
other: "See {{count}} new or updated topics"
|
||||
|
@ -546,6 +554,7 @@ en:
|
|||
"12": "Sent Items"
|
||||
"13": "Inbox"
|
||||
"14": "Pending"
|
||||
"15": "Drafts"
|
||||
|
||||
categories:
|
||||
all: "all categories"
|
||||
|
|
|
@ -776,6 +776,9 @@ en:
|
|||
no_replies:
|
||||
self: "You have not replied to any posts."
|
||||
others: "No replies."
|
||||
no_drafts:
|
||||
self: "You have no drafts."
|
||||
others: "No drafts."
|
||||
|
||||
topic_flag_types:
|
||||
spam:
|
||||
|
|
|
@ -736,6 +736,7 @@ Discourse::Application.routes.draw do
|
|||
|
||||
get "message-bus/poll" => "message_bus#poll"
|
||||
|
||||
resources :drafts, only: [:index]
|
||||
get "draft" => "draft#show"
|
||||
post "draft" => "draft#update"
|
||||
delete "draft" => "draft#destroy"
|
||||
|
|
|
@ -30,6 +30,10 @@ module UserGuardian
|
|||
is_me?(user) || is_admin?
|
||||
end
|
||||
|
||||
def can_see_drafts?(user)
|
||||
is_me?(user) || is_admin?
|
||||
end
|
||||
|
||||
def can_silence_user?(user)
|
||||
user && is_staff? && not(user.staff?)
|
||||
end
|
||||
|
|
|
@ -2350,6 +2350,24 @@ describe Guardian do
|
|||
end
|
||||
end
|
||||
|
||||
describe "can_see_drafts?" do
|
||||
it "won't allow a non-logged in user to see a user's drafts" do
|
||||
expect(Guardian.new.can_see_drafts?(user)).to be_falsey
|
||||
end
|
||||
|
||||
it "won't allow a user to see another user's drafts" do
|
||||
expect(Guardian.new(coding_horror).can_see_drafts?(user)).to be_falsey
|
||||
end
|
||||
|
||||
it "will allow user to see own drafts" do
|
||||
expect(Guardian.new(user).can_see_drafts?(user)).to be_truthy
|
||||
end
|
||||
|
||||
it "will allow an admin to see a user's drafts" do
|
||||
expect(Guardian.new(admin).can_see_drafts?(user)).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
describe "can_edit_email?" do
|
||||
context 'when allowed in settings' do
|
||||
before do
|
||||
|
|
|
@ -64,6 +64,34 @@ describe Draft do
|
|||
expect(Draft.count).to eq 0
|
||||
end
|
||||
|
||||
describe '#stream' do
|
||||
let(:public_post) { Fabricate(:post) }
|
||||
let(:public_topic) { public_post.topic }
|
||||
|
||||
let(:stream) do
|
||||
Draft.stream(user: @user)
|
||||
end
|
||||
|
||||
it "should include the correct number of drafts in the stream" do
|
||||
Draft.set(@user, "test", 0, "first")
|
||||
Draft.set(@user, "test2", 0, "second")
|
||||
expect(stream.count).to eq(2)
|
||||
end
|
||||
|
||||
it "should include the right topic id in a draft reply in the stream" do
|
||||
Draft.set(@user, "topic_#{public_topic.id}", 0, "hey")
|
||||
draft_row = stream.first
|
||||
expect(draft_row.topic_id).to eq(public_topic.id)
|
||||
end
|
||||
|
||||
it "should include the right draft username in the stream" do
|
||||
Draft.set(@user, "topic_#{public_topic.id}", 0, "hey")
|
||||
draft_row = stream.first
|
||||
expect(draft_row.draft_username).to eq(@user.username)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context 'key expiry' do
|
||||
it 'nukes new topic draft after a topic is created' do
|
||||
u = Fabricate(:user)
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
require 'rails_helper'
|
||||
|
||||
describe DraftsController do
|
||||
it 'requires you to be logged in' do
|
||||
get "/drafts.json"
|
||||
expect(response.status).to eq(403)
|
||||
end
|
||||
|
||||
it 'returns correct stream length after adding a draft' do
|
||||
user = sign_in(Fabricate(:user))
|
||||
Draft.set(user, 'xxx', 0, '{}')
|
||||
get "/drafts.json", params: { username: user.username }
|
||||
expect(response.status).to eq(200)
|
||||
parsed = JSON.parse(response.body)
|
||||
expect(parsed["drafts"].length).to eq(1)
|
||||
end
|
||||
|
||||
it 'has empty stream after deleting last draft' do
|
||||
user = sign_in(Fabricate(:user))
|
||||
Draft.set(user, 'xxx', 0, '{}')
|
||||
Draft.clear(user, 'xxx', 0)
|
||||
get "/drafts.json", params: { username: user.username }
|
||||
expect(response.status).to eq(200)
|
||||
parsed = JSON.parse(response.body)
|
||||
expect(parsed["drafts"].length).to eq(0)
|
||||
end
|
||||
end
|
|
@ -39,3 +39,18 @@ QUnit.test("Viewing Summary", async assert => {
|
|||
assert.ok(exists(".badges-section .badge-card"), "badges");
|
||||
assert.ok(exists(".top-categories-section .category-link"), "top categories");
|
||||
});
|
||||
|
||||
QUnit.test("Viewing Drafts", async assert => {
|
||||
await visit("/u/eviltrout/activity/drafts");
|
||||
assert.ok(exists(".user-stream"), "has drafts stream");
|
||||
assert.ok(
|
||||
$(".user-stream .user-stream-item-draft-actions").length,
|
||||
"has draft action buttons"
|
||||
);
|
||||
|
||||
await click(".user-stream button.resume-draft:eq(0)");
|
||||
assert.ok(
|
||||
exists(".d-editor-input"),
|
||||
"composer is visible after resuming a draft"
|
||||
);
|
||||
});
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
export default {
|
||||
"/draft.json": {
|
||||
draft:
|
||||
'{"reply":"dum de dum da ba.","action":"createTopic","title":"dum da ba dum dum","categoryId":null,"archetypeId":"regular","metaData":null,"composerTime":540879,"typingTime":3400}',
|
||||
draft_sequence: 0
|
||||
}
|
||||
};
|
|
@ -0,0 +1,61 @@
|
|||
export default {
|
||||
"/drafts.json": {
|
||||
drafts: [
|
||||
{
|
||||
excerpt: "A fun new topic for testing drafts. ",
|
||||
truncated: true,
|
||||
created_at: "2018-07-22T22:20:14.608Z",
|
||||
draft_key: "new_topic",
|
||||
sequence: 26,
|
||||
draft_username: "eviltrout",
|
||||
avatar_template: "/user_avatar/localhost/eviltrout/{size}/2_1.png",
|
||||
data:
|
||||
'{"reply":"A fun new topic for testing drafts. \\n","action":"createTopic","title":"This here is a new topic, friend","categoryId":3,"archetypeId":"regular","metaData":null,"composerTime":24532,"typingTime":2500}',
|
||||
topic_id: null,
|
||||
username: "eviltrout",
|
||||
name: null,
|
||||
user_id: 1,
|
||||
title: null,
|
||||
category_id: 3,
|
||||
archetype: null
|
||||
},
|
||||
{
|
||||
excerpt:
|
||||
"The last reply to this topic was 6 months ago. Your reply will bump the topic to the top of its list",
|
||||
truncated: true,
|
||||
created_at: "2018-07-20T19:04:32.023Z",
|
||||
draft_key: "topic_280",
|
||||
sequence: 0,
|
||||
draft_username: "eviltrout",
|
||||
avatar_template: "/letter_avatar_proxy/v2/letter/p/a87d85/{size}.png",
|
||||
data:
|
||||
'{"reply":"The last reply to this topic was 6 months ago. Your reply will bump the topic to the top of its list.","action":"reply","categoryId":8,"archetypeId":"regular","metaData":null,"composerTime":139499,"typingTime":6100}',
|
||||
topic_id: 280,
|
||||
username: "zogstrip",
|
||||
name: "zogstrip",
|
||||
user_id: 6,
|
||||
title: "Django hangs if I write gibberish",
|
||||
slug: "django-hangs-if-i-write-gibberish",
|
||||
category_id: 8,
|
||||
archetype: "regular"
|
||||
},
|
||||
{
|
||||
excerpt: "here goes a reply to a PM.",
|
||||
created_at: "2018-07-20T16:58:47.433Z",
|
||||
draft_key: "topic_93",
|
||||
sequence: 1,
|
||||
draft_username: "eviltrout",
|
||||
avatar_template: "/user_avatar/localhost/eviltrout/{size}/2_1.png",
|
||||
data:
|
||||
'{"reply":"here goes a reply to a PM.","action":"reply","categoryId":null,"postId":212,"archetypeId":"regular","whisper":false,"metaData":null,"composerTime":455711,"typingTime":5400}',
|
||||
topic_id: 93,
|
||||
username: "eviltrout",
|
||||
name: null,
|
||||
user_id: 1,
|
||||
title: "Hello dear friend, good to see you again",
|
||||
slug: "hello-dear-friend-good-to-see-you-again",
|
||||
archetype: "private_message"
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
|
@ -225,7 +225,15 @@ export default function() {
|
|||
return response({ category });
|
||||
});
|
||||
|
||||
this.get("/draft.json", () => response({}));
|
||||
this.get("/draft.json", request => {
|
||||
if (request.queryParams.draft_key === "new_topic") {
|
||||
return response(fixturesByUrl["/draft.json"]);
|
||||
}
|
||||
|
||||
return response({});
|
||||
});
|
||||
|
||||
this.get("/drafts.json", () => response(fixturesByUrl["/drafts.json"]));
|
||||
|
||||
this.put("/queued_posts/:queued_post_id", function(request) {
|
||||
return response({ queued_post: { id: request.params.queued_post_id } });
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
import UserDraft from "discourse/models/user-draft";
|
||||
import { NEW_TOPIC_KEY } from "discourse/models/composer";
|
||||
|
||||
QUnit.module("model:user-drafts");
|
||||
|
||||
QUnit.test("stream", assert => {
|
||||
const user = Discourse.User.create({ id: 1, username: "eviltrout" });
|
||||
const stream = user.get("userDraftsStream");
|
||||
assert.present(stream, "a user has a drafts stream by default");
|
||||
assert.equal(stream.get("itemsLoaded"), 0, "no items are loaded by default");
|
||||
assert.blank(stream.get("content"), "no content by default");
|
||||
});
|
||||
|
||||
QUnit.test("draft", assert => {
|
||||
const drafts = [
|
||||
UserDraft.create({
|
||||
draft_key: "topic_1",
|
||||
post_number: "10"
|
||||
}),
|
||||
UserDraft.create({
|
||||
draft_key: NEW_TOPIC_KEY
|
||||
})
|
||||
];
|
||||
|
||||
assert.equal(drafts.length, 2, "drafts count is right");
|
||||
assert.equal(
|
||||
drafts[1].get("draftType"),
|
||||
I18n.t("drafts.new_topic"),
|
||||
"loads correct draftType label"
|
||||
);
|
||||
});
|
Loading…
Reference in New Issue