diff --git a/.eslintrc b/.eslintrc index d2a13ae26..2b3ed3ba7 100644 --- a/.eslintrc +++ b/.eslintrc @@ -90,6 +90,7 @@ "no-undef": 2, "no-unused-vars": 2, "no-with": 2, + "no-this-before-super": 2, "semi": 2, "strict": 0, "valid-typeof": 2, diff --git a/app/assets/javascripts/discourse.js b/app/assets/javascripts/discourse.js index 5b16593a8..553d70fb4 100644 --- a/app/assets/javascripts/discourse.js +++ b/app/assets/javascripts/discourse.js @@ -152,21 +152,18 @@ window.Discourse = Ember.Application.createWithMixins(Discourse.Ajax, { }) }); -function proxyDep(propName, moduleFunc, msg) { - if (Discourse.hasOwnProperty(propName)) { return; } - Object.defineProperty(Discourse, propName, { - get: function() { - msg = msg || "import the module"; - Ember.warn("DEPRECATION: `Discourse." + propName + "` is deprecated, " + msg + "."); - return moduleFunc(); - } - }); +function RemovedObject(name) { + this._removedName = name; } -proxyDep('computed', function() { return require('discourse/lib/computed'); }); -proxyDep('Formatter', function() { return require('discourse/lib/formatter'); }); -proxyDep('PageTracker', function() { return require('discourse/lib/page-tracker').default; }); -proxyDep('URL', function() { return require('discourse/lib/url').default; }); -proxyDep('Quote', function() { return require('discourse/lib/quote').default; }); -proxyDep('debounce', function() { return require('discourse/lib/debounce').default; }); -proxyDep('View', function() { return Ember.View; }, "Use `Ember.View` instead"); +function methodMissing() { + console.warn("The " + this._removedName + " object has been removed from Discourse " + + "and your plugin needs to be updated."); +}; + +['reopen', 'registerButton'].forEach(function(m) { RemovedObject.prototype[m] = methodMissing; }); + +['discourse/views/post', 'discourse/components/post-menu'].forEach(function(moduleName) { + define(moduleName, [], function() { return new RemovedObject(moduleName); }); +}); + diff --git a/app/assets/javascripts/discourse/adapters/post-reply-history.js.es6 b/app/assets/javascripts/discourse/adapters/post-reply-history.js.es6 new file mode 100644 index 000000000..335c22b6b --- /dev/null +++ b/app/assets/javascripts/discourse/adapters/post-reply-history.js.es6 @@ -0,0 +1,10 @@ +import RestAdapter from 'discourse/adapters/rest'; + +export default RestAdapter.extend({ + find(store, type, findArgs) { + const maxReplies = Discourse.SiteSettings.max_reply_history; + return Discourse.ajax(`/posts/${findArgs.postId}/reply-history?max_replies=${maxReplies}`).then(replies => { + return { post_reply_histories: replies }; + }); + }, +}); diff --git a/app/assets/javascripts/discourse/adapters/post-reply.js.es6 b/app/assets/javascripts/discourse/adapters/post-reply.js.es6 new file mode 100644 index 000000000..f36299d00 --- /dev/null +++ b/app/assets/javascripts/discourse/adapters/post-reply.js.es6 @@ -0,0 +1,9 @@ +import RestAdapter from 'discourse/adapters/rest'; + +export default RestAdapter.extend({ + find(store, type, findArgs) { + return Discourse.ajax(`/posts/${findArgs.postId}/replies`).then(replies => { + return { post_replies: replies }; + }); + }, +}); diff --git a/app/assets/javascripts/discourse/components/actions-summary.js.es6 b/app/assets/javascripts/discourse/components/actions-summary.js.es6 deleted file mode 100644 index 533438852..000000000 --- a/app/assets/javascripts/discourse/components/actions-summary.js.es6 +++ /dev/null @@ -1,122 +0,0 @@ -import StringBuffer from 'discourse/mixins/string-buffer'; -import { iconHTML } from 'discourse/helpers/fa-icon'; -import { autoUpdatingRelativeAge } from 'discourse/lib/formatter'; -import { on } from 'ember-addons/ember-computed-decorators'; - -export default Ember.Component.extend(StringBuffer, { - tagName: 'section', - classNameBindings: [':post-actions', 'hidden'], - actionsSummary: Em.computed.alias('post.actionsWithoutLikes'), - emptySummary: Em.computed.empty('actionsSummary'), - hidden: Em.computed.and('emptySummary', 'post.notDeleted'), - usersByType: null, - - rerenderTriggers: ['actionsSummary.@each', 'post.deleted'], - - @on('init') - initUsersByType() { - this.set('usersByType', {}); - }, - - // This was creating way too many bound ifs and subviews in the handlebars version. - renderString(buffer) { - const usersByType = this.get('usersByType'); - - if (!this.get('emptySummary')) { - this.get('actionsSummary').forEach(function(c) { - const id = c.get('id'); - const users = usersByType[id] || []; - - buffer.push("
{{description}}
- {{#if post.cooked}} - - {{/if}}{{{i18n 'summary.enabled_description'}}}
- -{{else}} - {{#if topic.estimatedReadingTime}} -{{{i18n 'summary.description_time' count=topic.posts_count readingTime=topic.estimatedReadingTime}}}
- {{else}} -{{{i18n 'summary.description' count=topic.posts_count}}}
- {{/if}} - - -{{/if}} diff --git a/app/assets/javascripts/discourse/templates/components/topic-participant.hbs b/app/assets/javascripts/discourse/templates/components/topic-participant.hbs deleted file mode 100644 index 0389b002f..000000000 --- a/app/assets/javascripts/discourse/templates/components/topic-participant.hbs +++ /dev/null @@ -1,6 +0,0 @@ - - {{#if showPostCount}} - {{unbound participant.post_count}} - {{/if}} - {{avatar participant imageSize="medium"}} - diff --git a/app/assets/javascripts/discourse/templates/embedded-post.hbs b/app/assets/javascripts/discourse/templates/embedded-post.hbs deleted file mode 100644 index 03bfc30a2..000000000 --- a/app/assets/javascripts/discourse/templates/embedded-post.hbs +++ /dev/null @@ -1,16 +0,0 @@ -${description}
` })); + + if (attrs.cooked) { + contents.push(new RawHtml({ html: ` ` })); + } + + return [ + h('div.topic-avatar', iconNode(icons[attrs.actionCode] || 'exclamation')), + h('div.small-action-desc', contents) + ]; + } +}); diff --git a/app/assets/javascripts/discourse/widgets/post-stream.js.es6 b/app/assets/javascripts/discourse/widgets/post-stream.js.es6 new file mode 100644 index 000000000..723ef1ab4 --- /dev/null +++ b/app/assets/javascripts/discourse/widgets/post-stream.js.es6 @@ -0,0 +1,75 @@ +import { createWidget } from 'discourse/widgets/widget'; +import transformPost from 'discourse/lib/transform-post'; + +const DAY = 1000 * 60 * 60 * 24; + +export default createWidget('post-stream', { + tagName: 'div.post-stream', + + + html(attrs) { + const posts = attrs.posts || []; + const postArray = posts.toArray(); + + const result = []; + + const before = attrs.gaps && attrs.gaps.before ? attrs.gaps.before : {}; + const after = attrs.gaps && attrs.gaps.before ? attrs.gaps.after : {}; + + let prevPost; + let prevDate; + + for (let i=0; i${this.description(attrs)}
` }); + } +}); + +export default createWidget('toggle-topic-summary', { + tagName: 'section.information.toggle-summary', + html(attrs) { + return [ this.attach('toggle-summary-description', attrs), + this.attach('button', { + className: 'btn btn-primary', + label: attrs.topicSummaryEnabled ? 'summary.disable' : 'summary.enable', + action: 'toggleSummary' + }) ]; + } +}); diff --git a/app/assets/javascripts/discourse/widgets/topic-map.js.es6 b/app/assets/javascripts/discourse/widgets/topic-map.js.es6 new file mode 100644 index 000000000..00d6f7e93 --- /dev/null +++ b/app/assets/javascripts/discourse/widgets/topic-map.js.es6 @@ -0,0 +1,219 @@ +import { createWidget } from 'discourse/widgets/widget'; +import { h } from 'virtual-dom'; +import { avatarImg, avatarFor } from 'discourse/widgets/post'; +import { dateNode, numberNode } from 'discourse/helpers/node'; + +const LINKS_SHOWN = 5; + +function renderParticipants(userFilters, participants) { + if (!participants) { return; } + + userFilters = userFilters || []; + return participants.map(p => { + return this.attach('topic-participant', p, { state: { toggled: userFilters.contains(p.username) } }); + }); +} + +createWidget('topic-map-show-links', { + tagName: 'div.link-summary', + html(attrs) { + return h('a', I18n.t('topic_map.links_shown', { totalLinks: attrs.totalLinks })); + }, + + click() { + this.sendWidgetAction('showAllLinks'); + } +}); + +createWidget('topic-participant', { + html(attrs, state) { + const linkContents = [avatarImg('medium', { username: attrs.username, template: attrs.avatar_template })]; + + if (attrs.post_count > 2) { + linkContents.push(h('span.post-count', attrs.post_count.toString())); + } + + return h('a.poster', { className: state.toggled ? 'toggled' : null, attributes: { title: attrs.username } }, + linkContents + ); + }, + + click() { + this.sendWidgetAction('toggleParticipant', this.attrs); + } +}); + +createWidget('topic-map-summary', { + tagName: 'section.map', + + buildClasses(attrs, state) { + if (state.collapsed) { return 'map-collapsed'; } + }, + + html(attrs, state) { + const contents = []; + contents.push(h('li', + [ + h('h4', I18n.t('created_lowercase')), + avatarFor('tiny', { username: attrs.createdByUsername, template: attrs.createdByAvatarTemplate }), + dateNode(attrs.topicCreatedAt) + ] + )); + contents.push(h('li', + h('a', { attributes: { href: attrs.lastPostUrl } }, [ + h('h4', I18n.t('last_reply_lowercase')), + avatarFor('tiny', { username: attrs.lastPostUsername, template: attrs.lastPostAvatarTemplate }), + dateNode(attrs.lastPostAt) + ]) + )); + contents.push(h('li', [ + numberNode(attrs.topicReplyCount), + h('h4', I18n.t('replies_lowercase', { count: attrs.topicReplyCount })) + ])); + contents.push(h('li.secondary', [ + numberNode(attrs.topicViews, { className: attrs.topicViewsHeat }), + h('h4', I18n.t('views_lowercase', { count: attrs.topicViews })) + ])); + contents.push(h('li.secondary', [ + numberNode(attrs.participantCount), + h('h4', I18n.t('users_lowercase', { count: attrs.participantCount })) + ])); + + if (attrs.topicLikeCount) { + contents.push(h('li.secondary', [ + numberNode(attrs.likeCount), + h('h4', I18n.t('likes_lowercase', { count: attrs.likeCount })) + ])); + } + + if (attrs.topicLinkLength > 0) { + contents.push(h('li.secondary', [ + numberNode(attrs.topicLinkLength), + h('h4', I18n.t('links_lowercase', { count: attrs.topicLinkLength })) + ])); + } + + if (state.collapsed && attrs.topicPostsCount > 2 && attrs.participants.length > 0) { + const participants = renderParticipants.call(this, attrs.userFilters, attrs.participants.slice(0, 3)); + contents.push(h('li.avatars', participants)); + } + + return h('ul.clearfix', contents); + } +}); + +createWidget('topic-map-link', { + tagName: 'a.topic-link.track-link', + + buildClasses(attrs) { + if (attrs.attachment) { return 'attachment'; } + }, + + buildAttributes(attrs) { + return { href: attrs.url, + target: "_blank", + 'data-user-id': attrs.user_id, + 'data-ignore-post-id': 'true', + title: attrs.url }; + }, + + html(attrs) { + if (attrs.title) { return attrs.title; } + return attrs.url; + } +}); + +createWidget('topic-map-expanded', { + tagName: 'section.topic-map-expanded', + + defaultState() { + return { allLinksShown: false }; + }, + + html(attrs, state) { + const avatars = h('section.avatars.clearfix', [ + h('h3', I18n.t('topic_map.participants_title')), + renderParticipants.call(this, attrs.userFilters, attrs.participants) + ]); + + const result = [avatars]; + if (attrs.topicLinks) { + + const toShow = state.allLinksShown ? attrs.topicLinks : attrs.topicLinks.slice(0, LINKS_SHOWN); + const links = toShow.map(l => { + + let host = ''; + if (l.title && l.title.length) { + const domain = l.domain; + if (domain && domain.length) { + const s = domain.split('.'); + host = h('span.domain', s[s.length-2] + "." + s[s.length-1]); + } + } + + return h('tr', [ + h('td', + h('span.badge.badge-notification.clicks', { + attributes: { title: I18n.t('topic_map.clicks', { count: l.clicks }) } + }, l.clicks.toString()) + ), + h('td', [this.attach('topic-map-link', l), ' ', host]) + ]); + }); + + const showAllLinksContent = [ + h('h3', I18n.t('topic_map.links_title')), + h('table.topic-links', links) + ]; + + if (!state.allLinksShown && links.length < attrs.topicLinks.length) { + showAllLinksContent.push(this.attach('topic-map-show-links', { totalLinks: attrs.topicLinks.length })); + } + + const section = h('section.links', showAllLinksContent); + result.push(section); + } + return result; + }, + + showAllLinks() { + this.state.allLinksShown = true; + } +}); + +export default createWidget('topic-map', { + tagName: 'div.topic-map', + buildKey: attrs => `topic-map-${attrs.id}`, + + defaultState() { + return { collapsed: true }; + }, + + html(attrs, state) { + const nav = h('nav.buttons', this.attach('button', { + title: 'topic.toggle_information', + icon: state.collapsed ? 'chevron-down' : 'chevron-up', + action: 'toggleMap', + className: 'btn', + })); + + const contents = [nav, this.attach('topic-map-summary', attrs, { state })]; + + if (!state.collapsed) { + contents.push(this.attach('topic-map-expanded', attrs)); + } + + if (attrs.hasTopicSummary) { + contents.push(this.attach('toggle-topic-summary', attrs)); + } + + if (attrs.showPMMap) { + contents.push(this.attach('private-message-map', attrs)); + } + return contents; + }, + + toggleMap() { + this.state.collapsed = !this.state.collapsed; + } +}); diff --git a/app/assets/javascripts/discourse/widgets/widget.js.es6 b/app/assets/javascripts/discourse/widgets/widget.js.es6 new file mode 100644 index 000000000..aeff96192 --- /dev/null +++ b/app/assets/javascripts/discourse/widgets/widget.js.es6 @@ -0,0 +1,214 @@ +import { WidgetClickHook, WidgetClickOutsideHook } from 'discourse/widgets/click-hook'; +import { h } from 'virtual-dom'; + +function emptyContent() { } + +const _registry = {}; +const _dirty = {}; + +export function keyDirty(key) { + _dirty[key] = true; +} + +function drawWidget(builder, attrs, state) { + const properties = {}; + + if (this.buildClasses) { + let classes = this.buildClasses(attrs, state) || []; + if (!Array.isArray(classes)) { classes = [classes]; } + if (classes.length) { + properties.className = classes.join(' '); + } + } + if (this.buildId) { + properties.id = this.buildId(attrs); + } + + if (this.buildAttributes) { + properties.attributes = this.buildAttributes(attrs); + } + if (this.clickOutside) { + properties['widget-click-outside'] = new WidgetClickOutsideHook(this); + } + if (this.click) { + properties['widget-click'] = new WidgetClickHook(this); + } + + const attributes = properties['attributes'] || {}; + properties.attributes = attributes; + if (this.title) { + attributes.title = I18n.t(this.title); + } + + return h(this.tagName || 'div', properties, this.html(attrs, state)); +} + +export function createWidget(name, opts) { + const result = class CustomWidget extends Widget {}; + + if (name) { + _registry[name] = result; + } + + opts.html = opts.html || emptyContent; + opts.draw = drawWidget; + + Object.keys(opts).forEach(k => result.prototype[k] = opts[k]); + return result; +} + +export default class Widget { + constructor(attrs, container, opts) { + opts = opts || {}; + this.attrs = attrs || {}; + this.mergeState = opts.state; + this.container = container; + this.model = opts.model; + + this.key = this.buildKey ? this.buildKey(attrs) : null; + + this.site = container.lookup('site:main'); + this.siteSettings = container.lookup('site-settings:main'); + this.currentUser = container.lookup('current-user:main'); + this.store = container.lookup('store:main'); + } + + defaultState() { + return {}; + } + + destroy() { + console.log('destroy called'); + } + + render(prev) { + if (prev && prev.state) { + this.state = prev.state; + } else { + this.state = this.defaultState(); + } + + // Sometimes we pass state down from the parent + if (this.mergeState) { + this.state = _.merge(this.state, this.mergeState); + } + + if (prev && prev.shadowTree) { + this.shadowTree = true; + if (!_dirty[prev.key]) { return prev.vnode; } + } + + return this.draw(h, this.attrs, this.state); + } + + _findAncestorWithProperty(property) { + let widget = this; + while (widget) { + const value = widget[property]; + if (value) { + return widget; + } + widget = widget.parentWidget; + } + } + + _findView() { + const widget = this._findAncestorWithProperty('_emberView'); + if (widget) { + return widget._emberView; + } + } + + attach(widgetName, attrs, opts) { + let WidgetClass = _registry[widgetName]; + + if (!WidgetClass) { + if (!this.container) { + console.error("couldn't find container"); + return; + } + WidgetClass = this.container.lookupFactory(`widget:${widgetName}`); + } + + if (WidgetClass) { + const result = new WidgetClass(attrs, this.container, opts); + result.parentWidget = this; + return result; + } else { + throw `Couldn't find ${widgetName} factory`; + } + } + + scheduleRerender() { + let widget = this; + while (widget) { + if (widget.shadowTree) { + keyDirty(widget.key); + } + + const emberView = widget._emberView; + if (emberView) { + return emberView.queueRerender(); + } + widget = widget.parentWidget; + } + } + + sendComponentAction(name, param) { + const view = this._findAncestorWithProperty('_emberView'); + + let promise; + if (view) { + // Peek into ember internals to allow us to return promises from actions + const ev = view._emberView; + const target = ev.get('targetObject'); + + const actionName = ev.get(name); + if (!actionName) { + Ember.warn(`${name} not found`); + return; + } + + if (target) { + const actions = target._actions || target.actionHooks; + const method = actions[actionName]; + if (method) { + promise = method.call(target, param); + if (!promise || !promise.then) { + promise = Ember.RSVP.resolve(promise); + } + } else { + return ev.sendAction(name, param); + } + } + } + + if (promise) { + return promise.then(() => this.scheduleRerender()); + } + } + + findAncestorModel() { + const modelWidget = this._findAncestorWithProperty('model'); + if (modelWidget) { + return modelWidget.model; + } + } + + sendWidgetAction(name, param) { + const widget = this._findAncestorWithProperty(name); + if (widget) { + const result = widget[name](param); + if (result && result.then) { + return result.then(() => this.scheduleRerender()); + } else { + this.scheduleRerender(); + return result; + } + } + + return this.sendComponentAction(name, param || this.findAncestorModel()); + } +} + +Widget.prototype.type = 'Thunk'; diff --git a/app/assets/javascripts/main_include.js b/app/assets/javascripts/main_include.js index 43379cc09..dd6a27e13 100644 --- a/app/assets/javascripts/main_include.js +++ b/app/assets/javascripts/main_include.js @@ -58,11 +58,9 @@ //= require ./discourse/models/user-badge //= require ./discourse/controllers/discovery-sortable //= require ./discourse/controllers/navigation/default -//= require ./discourse/views/grouped //= require ./discourse/views/container //= require ./discourse/views/modal-body //= require ./discourse/views/flag -//= require ./discourse/views/cloaked //= require ./discourse/components/combo-box //= require ./discourse/components/edit-category-panel //= require ./discourse/views/button @@ -110,3 +108,4 @@ //= require_tree ./discourse/pre-initializers //= require_tree ./discourse/initializers //= require_tree ./discourse/services +//= require_tree ./discourse/widgets diff --git a/app/assets/javascripts/vendor.js b/app/assets/javascripts/vendor.js index 29ee2c532..d2143d320 100644 --- a/app/assets/javascripts/vendor.js +++ b/app/assets/javascripts/vendor.js @@ -38,4 +38,6 @@ //= require break_string //= require buffered-proxy //= require jquery.autoellipsis-1.0.10.min.js +//= require virtual-dom +//= require virtual-dom-amd //= require_tree ./discourse/ember diff --git a/app/assets/stylesheets/desktop/topic-post.scss b/app/assets/stylesheets/desktop/topic-post.scss index e5db38f83..0d4aa675d 100644 --- a/app/assets/stylesheets/desktop/topic-post.scss +++ b/app/assets/stylesheets/desktop/topic-post.scss @@ -19,7 +19,7 @@ h1 .topic-statuses .topic-status i { max-height: 40px; } -.post-cloak { +.topic-body { padding: 0; &:first-of-type { @@ -208,7 +208,6 @@ nav.post-controls { bottom: -2px; right: 15px; z-index: 1000; - display: none; h3 { margin-top: 0; @@ -1003,9 +1002,9 @@ and (max-width : 870px) { width: 45px; } - .post-cloak .reply-to-tab { + .topic-post .reply-to-tab { right: 15%; - } + } .topic-body { box-sizing: border-box; diff --git a/app/assets/stylesheets/mobile/topic.scss b/app/assets/stylesheets/mobile/topic.scss index 75b772802..258307305 100644 --- a/app/assets/stylesheets/mobile/topic.scss +++ b/app/assets/stylesheets/mobile/topic.scss @@ -163,7 +163,7 @@ position: absolute; } -.post-cloak:last-of-type {padding-bottom: 40px;} +.topic-post:last-of-type {padding-bottom: 40px;} .heatmap-high {color: scale-color($danger, $lightness: -25%) !important;} .heatmap-med {color: $danger !important;} diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 97dbc78ac..5ae311e51 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -1530,17 +1530,14 @@ en: like: "Undo like" vote: "Undo vote" people: - off_topic: "{{icons}} flagged this as off-topic" - spam: "{{icons}} flagged this as spam" - spam_with_url: "{{icons}} flagged this as spam" - inappropriate: "{{icons}} flagged this as inappropriate" - notify_moderators: "{{icons}} notified moderators" - notify_moderators_with_url: "{{icons}} notified moderators" - notify_user: "{{icons}} sent a message" - notify_user_with_url: "{{icons}} sent a message" - bookmark: "{{icons}} bookmarked this" - like: "{{icons}} liked this" - vote: "{{icons}} voted for this" + off_topic: "flagged this as off-topic" + spam: "flagged this as spam" + inappropriate: "flagged this as inappropriate" + notify_moderators: "notified moderators" + notify_user: "sent a message" + bookmark: "bookmarked this" + like: "liked this" + vote: "voted for this" by_you: off_topic: "You flagged this as off-topic" spam: "You flagged this as spam" diff --git a/config/locales/server.ar.yml b/config/locales/server.ar.yml index a63a9a598..9cfde3e21 100644 --- a/config/locales/server.ar.yml +++ b/config/locales/server.ar.yml @@ -928,7 +928,6 @@ ar: suppress_reply_directly_above: "لا تظهر للتوسع في الرد لعلى وظيفة عندما يكون هناك سوى رد واحد مباشرة فوق هذا المنصب." suppress_reply_when_quoting: "لا تظهر للتوسع في الرد لعلى وظيفة عندما يكون هناك سوى رد واحد مباشرة فوق هذا المنصب." max_reply_history: "الحد الأقصى لعدد الردود على توسيع عند توسيع في الرد ل" - experimental_reply_expansion: "إخفاء ردود المتوسطة عند توسيع ردا على (تجريبي)" topics_per_period_in_top_summary: "عدد من أهم الموضوعات هو مبين في موجز أعلى الافتراضية المواضيع." topics_per_period_in_top_page: "عدد من أهم الموضوعات هو مبين في موجز أعلى الافتراضية المواضيع." redirect_users_to_top_page: "إعادة توجيه تلقائيا للمستخدمين الجدد وغائبة لمدة طويلة إلى أعلى الصفحة." diff --git a/config/locales/server.de.yml b/config/locales/server.de.yml index 66051a31f..f3d2eb192 100644 --- a/config/locales/server.de.yml +++ b/config/locales/server.de.yml @@ -758,7 +758,6 @@ de: suppress_reply_directly_above: "Verstecke das erweiterbare „Antwort auf“-Feld in einem Beitrag, wenn der beantwortete Beitrag direkt darüber angezeigt wird." suppress_reply_when_quoting: "Verstecke das erweiterbare „Antwort auf“-Feld in einem Beitrag, wenn der Beitrag den beantworteten Beitrag zitiert." max_reply_history: "Maximale Anzahl an Antworten beim Ausklappen von in-reply-to" - experimental_reply_expansion: "Verstecke dazwischenliegende Beiträge, wenn der beantwortete Beitrag erweitert wird (experimentell)." topics_per_period_in_top_summary: "Anzahl der Themen, die in der Top-Themübersicht angezeigt werden." topics_per_period_in_top_page: "Anzahl der Themen, die in der mit \"Mehr zeigen\" erweiterten Top-Themenübersicht angezeigt werden." redirect_users_to_top_page: "Verweise neue und länger abwesende Nutzer automatisch zur Top Übersichtsseite" diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index f0f4c26cd..7c1a72820 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -877,9 +877,6 @@ en: suppress_reply_directly_above: "Don't show the expandable in-reply-to on a post when there is only a single reply directly above this post." suppress_reply_when_quoting: "Don't show the expandable in-reply-to on a post when post quotes reply." max_reply_history: "Maximum number of replies to expand when expanding in-reply-to" - - experimental_reply_expansion: "Hide intermediate replies when expanding a reply to (experimental)" - topics_per_period_in_top_summary: "Number of top topics shown in the default top topics summary." topics_per_period_in_top_page: "Number of top topics shown on the expanded 'Show More' top topics." redirect_users_to_top_page: "Automatically redirect new and long absent users to the top page." diff --git a/config/locales/server.es.yml b/config/locales/server.es.yml index e4cd55c40..5cb5d2a30 100644 --- a/config/locales/server.es.yml +++ b/config/locales/server.es.yml @@ -773,7 +773,6 @@ es: suppress_reply_directly_above: "No mostrar el en-respuesta-a desplegable en un post cuando solo hay una sola respuesta justo encima del post." suppress_reply_when_quoting: "No mostrar el desplegable en-respuesta-a en un post cuando el post cite la respuesta." max_reply_history: "Número máximo de respuestas a mostrar al expandir en-respuesta-a" - experimental_reply_expansion: "Ocultar respuestas intermedias cuando se expande una respuesta (experimental)" topics_per_period_in_top_summary: "Número de mejores temas mostrados en el resumen de mejores temas." topics_per_period_in_top_page: "Número de mejores temas mostrados en la vista expandida al clicar en 'ver más'." redirect_users_to_top_page: "Redirigir automáticamente a los nuevos usuarios y a los ausentes de larga duración a la página de mejores temas." diff --git a/config/locales/server.fa_IR.yml b/config/locales/server.fa_IR.yml index 5b66093ba..d96cbb7f1 100644 --- a/config/locales/server.fa_IR.yml +++ b/config/locales/server.fa_IR.yml @@ -699,7 +699,6 @@ fa_IR: suppress_reply_directly_above: "in-reply-to قابل بزرگ شدن را نشان نده در یک نوشته وقتی فقط یک پاسخ بالای این نوشته است." suppress_reply_when_quoting: "in-reply-to قابل بزرگ شدن را نشان نده در یک نوشته وقتی به یک نوشته پاسخ داده می شود." max_reply_history: "حداکثر تعداد پاسخ ها به توسعه زمان گسترش in-reply-to" - experimental_reply_expansion: "پاسخ های میانی را مخفی کن زمان توسعه یک پاسخ به (آزمایشی) " topics_per_period_in_top_summary: "تعداد بهتریت جستارهای نشان داده شده در بخش پیش فرض خلاصه بهترین جستارها." topics_per_period_in_top_page: "تعداد جستارهای خوب نشان داده شود در بخش گسترش یافته \" بیشتر نشان بده\" بهترین جستارها. " redirect_users_to_top_page: "بطور خودکار کاربران جدید و کاربران غایب را به بهترین صفحه هدایت کن." diff --git a/config/locales/server.fi.yml b/config/locales/server.fi.yml index 48c387f36..143c58ccf 100644 --- a/config/locales/server.fi.yml +++ b/config/locales/server.fi.yml @@ -781,7 +781,6 @@ fi: suppress_reply_directly_above: "Älä näytä vastauksena-painiketta viestin yläreunassa, jos viestissä on vastattu vain edelliseen viestiin." suppress_reply_when_quoting: "Älä näytä vastauksena-painiketta viestin yläreunassa, kun viestissä on lainaus." max_reply_history: "Maksimimäärä vastauksia, jotka avataan klikattaessa 'vastauksena' painiketta" - experimental_reply_expansion: "Piilota välilliset vastaukset, kun 'vastauksena' avataan (kokeellinen)" topics_per_period_in_top_summary: "Ketjujen lukumäärä, joka näytetään oletuksena Huiput-listauksissa." topics_per_period_in_top_page: "Ketjujen lukumäärä, joka näytetään laajennetussa Huiput-listauksessa." redirect_users_to_top_page: "Ohjaa uudet ja kauan poissa olleet käyttäjät automaattisesti huiput-sivulle." diff --git a/config/locales/server.fr.yml b/config/locales/server.fr.yml index d07d23a93..105b6be96 100644 --- a/config/locales/server.fr.yml +++ b/config/locales/server.fr.yml @@ -775,7 +775,6 @@ fr: suppress_reply_directly_above: "Ne pas afficher 'en réponse à' sur un message quand la seule réponse est juste en dessus de ce dernier." suppress_reply_when_quoting: "Ne pas affiché le panneau \"En réponse à\" sur un message qui répond à une citation." max_reply_history: "Nombre maximum de réponses à développer lors du développement d'une \"réponse à\"" - experimental_reply_expansion: "Masquer les réponses intermédiaires lors de l'ouverture d'une répondre à (expérimental)" topics_per_period_in_top_summary: "Nombre de meilleurs sujets affichés dans le résumé par défaut des meilleurs sujets." topics_per_period_in_top_page: "Nombre de meilleurs sujets affichés lorsqu'on sélectionne \"Voir plus\" des meilleurs sujets." redirect_users_to_top_page: "Rediriger automatiquement les nouveaux utilisateurs et les longues absences sur la page Top." diff --git a/config/locales/server.he.yml b/config/locales/server.he.yml index 008299cdd..e79e6d50e 100644 --- a/config/locales/server.he.yml +++ b/config/locales/server.he.yml @@ -727,7 +727,6 @@ he: suppress_reply_directly_above: "אל תציגו את את אפשרות ההרחבה \"בתגובה ל..\" לפרסום כאשר יש רק תגובה אחת ישירה מעל לפרסום זה." suppress_reply_when_quoting: "אל תציגו את הפרסום המקורי בפרסומים שמצטטים תגובות" max_reply_history: "מספר התגובות המקסימלי להרחבה כאשר מרחיבים \"בתגובה ל\"" - experimental_reply_expansion: "החבא תגובות ביניים כאשר מרחיבים תגובה (ניסיוני)" topics_per_period_in_top_summary: "מספר הנושאים המוצגים בבריכת המחדל של סיכום הנושאים." topics_per_period_in_top_page: "מספר הנושאים הראשונים המוצגים בתצוגה המורחבת של \"הצג עוד\"." redirect_users_to_top_page: "כוון באופן אוטומטי משתמשים חדשים וכאלה שנעדרו במשך זמן לראש העמוד." diff --git a/config/locales/server.it.yml b/config/locales/server.it.yml index 3d12040e8..969ba7c39 100644 --- a/config/locales/server.it.yml +++ b/config/locales/server.it.yml @@ -728,7 +728,6 @@ it: suppress_reply_directly_above: "Non mostrare in-risposta-a espandibile in un messaggio quando c'è una sola risposta sopra quel messaggio. " suppress_reply_when_quoting: "Non mostrare in-risposta-a espandibile in un messaggio quando il messaggio include la citazione." max_reply_history: "Numero massimo di risposte da espandere quando si espande in-risposta-a" - experimental_reply_expansion: "Nascondi le risposte intermedie quando si espande una risposta (sperimentale)" topics_per_period_in_top_summary: "Numero di argomenti di punta mostrati nel riepilogo di default." topics_per_period_in_top_page: "Numero di argomenti di punta mostrati nella vista espansa 'Mostra Altro'" redirect_users_to_top_page: "Redirigi automaticamente i nuovi utenti e quelli assenti da tempo sulla pagina degli argomenti di punta." diff --git a/config/locales/server.ja.yml b/config/locales/server.ja.yml index dcfe7301d..0d4381c44 100644 --- a/config/locales/server.ja.yml +++ b/config/locales/server.ja.yml @@ -624,7 +624,6 @@ ja: suppress_reply_directly_above: "ポストに回答が1つしかない場合、ポストのin-reply-toを表示しない" suppress_reply_when_quoting: "ポストが引用返信だった場合、ポストのin-reply-toを表示しない" max_reply_history: "回答のin-reply-toを展開する最大数" - experimental_reply_expansion: "回答を展開するときに、その間にある回答を非表示にする(実験)" topics_per_period_in_top_summary: "デフォルトのトピックサマリに表示されるトップトピックの数" topics_per_period_in_top_page: "'もっと見る'を展開したときに表示するトップトピックの数" redirect_users_to_top_page: "新規ユーザーと長く不在のユーザーをトップページに自動的にリダイレクトさせる" diff --git a/config/locales/server.ko.yml b/config/locales/server.ko.yml index 14b7ff0b0..8c7773edd 100644 --- a/config/locales/server.ko.yml +++ b/config/locales/server.ko.yml @@ -643,7 +643,6 @@ ko: suppress_reply_directly_above: "단 하나의 댓글 위의 글이 하나 있는 상황에서 '~에 대한 댓글'을 보여주지 않음." suppress_reply_when_quoting: "글안에 답글이 인용될 때 in-reply-to를 보여주지 않습니다." max_reply_history: "덧글 확장해서 보여지는 최대 갯수" - experimental_reply_expansion: "답글을 펼쳤을 때 중간 답글을 숨깁니다(실험 중)" topics_per_period_in_top_summary: "인기 글타래 요약에 기본으로 보여질 글타래 수" topics_per_period_in_top_page: "인기 글타래에서 '더 보기'를 요청할 시 보여질 글타래 수" redirect_users_to_top_page: "자동으로 신규 사용자와 오래간만에 들어온 사용자를 탑 페이지로 리다이렉트 시킴" diff --git a/config/locales/server.nl.yml b/config/locales/server.nl.yml index b1f023945..75d719945 100644 --- a/config/locales/server.nl.yml +++ b/config/locales/server.nl.yml @@ -746,7 +746,6 @@ nl: suppress_reply_directly_above: "Verberg de uitklapbare 'in antwoord op' knop bij een bericht als er maar één reactie direct boven staat." suppress_reply_when_quoting: "Verberg de uitklapbare 'in antwoord op' knop bij een bericht als er er gequoteerd is." max_reply_history: "Maximaal aantal uit te klappen antwoorden als een 'in antwoord op' uitgeklapt wordt." - experimental_reply_expansion: "Verberg tussenliggende antwoorden als een antwoord uitgeklapt wordt (experimenteel)" topics_per_period_in_top_summary: "Aantal topics in het top-topics overzicht" topics_per_period_in_top_page: "Aantal topics in het uitgeklapte ´Meer...´ top-topics overzicht" redirect_users_to_top_page: "Stuur nieuwe en lang-niet-geziene gebruikers door naar de top-pagina" diff --git a/config/locales/server.pt.yml b/config/locales/server.pt.yml index f90e50406..adabb2c27 100644 --- a/config/locales/server.pt.yml +++ b/config/locales/server.pt.yml @@ -781,7 +781,6 @@ pt: suppress_reply_directly_above: "Não mostrar em-resposta-a expansível quando há apenas uma única resposta diretamente acima desta publicação." suppress_reply_when_quoting: "Não mostraR em-resposta-a expansível numa mensagem quando esta cita uma resposta." max_reply_history: "Número máximo de respostas a serem expandidas quando se expande em-resposta-a" - experimental_reply_expansion: "Esconder respostas intermédias ao expandir uma resposta-a (experimental)" topics_per_period_in_top_summary: "Número de tópicos principais mostrados no resumo padrão de tópicos principais." topics_per_period_in_top_page: "Número de tópicos principais mostrados em 'Mostrar Mais' tópicos principais expandido." redirect_users_to_top_page: "Redirecionar automaticamente os utilizadores novos e ausentes por períodos longos para o topo da página." diff --git a/config/locales/server.pt_BR.yml b/config/locales/server.pt_BR.yml index 160aca462..d21721249 100644 --- a/config/locales/server.pt_BR.yml +++ b/config/locales/server.pt_BR.yml @@ -747,7 +747,6 @@ pt_BR: suppress_reply_directly_above: "Não mostrar a em-resposta-à expansível em um post quando há apenas uma única resposta diretamente acima deste post." suppress_reply_when_quoting: "Não mostrar a em-resposta-à expansível em um post quando o post cita a resposta." max_reply_history: "Número máximo de respostas para expandir quando expandindo em-resposta-à" - experimental_reply_expansion: "Esconder respostas intermediárias quando expandindo uma resposta à (experimental)" topics_per_period_in_top_summary: "Número de melhores tópicos mostrados no sumário padrão de melhores tópicos." topics_per_period_in_top_page: "Número de melhores tópicos mostrados no 'Exibir Mais' melhores tópicos quando expandido." redirect_users_to_top_page: "Automaticamente redirecionar usuários novos e há muito ausentes para a página melhores." diff --git a/config/locales/server.ru.yml b/config/locales/server.ru.yml index 415b548b5..c3b75bbc7 100644 --- a/config/locales/server.ru.yml +++ b/config/locales/server.ru.yml @@ -767,7 +767,6 @@ ru: suppress_reply_directly_above: "Не показывать разворачивемый блок \"в ответ на\" для сообщения, если есть всего лишь одно сообщение непосредственно выше." suppress_reply_when_quoting: "Не показывать разворачивемый блок \"в ответ на\", если сообщение уже содержит цитату." max_reply_history: "Максимальное число разворачивающихся ответов в блоке \"в ответ на\"" - experimental_reply_expansion: "Скрыть промежуточные ответы, когда раскрывается все ответы (эксперементальная функция)" topics_per_period_in_top_summary: "Количество рекомендованных тем, отображаемых внизу текущей темы." topics_per_period_in_top_page: "Количество рекомендованных тем, отображаемых при нажатии 'Показать больше' в низу текущей темы." redirect_users_to_top_page: "Автоматически перенаправлять новых и давно отсутствующих пользователей к началу страницы." diff --git a/config/locales/server.sq.yml b/config/locales/server.sq.yml index 63eb1ece9..a2c0e7846 100644 --- a/config/locales/server.sq.yml +++ b/config/locales/server.sq.yml @@ -698,7 +698,6 @@ sq: suppress_reply_directly_above: "Don't show the expandable in-reply-to on a post when there is only a single reply directly above this post." suppress_reply_when_quoting: "Don't show the expandable in-reply-to on a post when post quotes reply." max_reply_history: "Maximum number of replies to expand when expanding in-reply-to" - experimental_reply_expansion: "Hide intermediate replies when expanding a reply to (experimental)" topics_per_period_in_top_summary: "Number of top topics shown in the default top topics summary." topics_per_period_in_top_page: "Number of top topics shown on the expanded 'Show More' top topics." redirect_users_to_top_page: "Automatically redirect new and long absent users to the top page." diff --git a/config/locales/server.tr_TR.yml b/config/locales/server.tr_TR.yml index c56bf44e2..0f044e9a0 100644 --- a/config/locales/server.tr_TR.yml +++ b/config/locales/server.tr_TR.yml @@ -713,7 +713,6 @@ tr_TR: suppress_reply_directly_above: "Bu gönderinin direk üstünde sadece tek bir cevap varsa, gönderideki açılabilir hangi-cevaba-istinaden-cevapla bölümünü gösterme." suppress_reply_when_quoting: "Gönderi cevabı alıntılarsa, gönderideki açılabilir hangi-cevaba-istinaden-cevapla bölümünü gösterme." max_reply_history: "Hangi-cevaba-istinaden-cevapla bölümü açılınca gösterilecek en fazla cevap sayısı" - experimental_reply_expansion: "Hangi-cevaba-istinaden-cevapla bölümü açılınca ara cevapları gizle (deneysel)" topics_per_period_in_top_summary: "Popüler konular özetinde gösterilen popüler konu sayısı." topics_per_period_in_top_page: "'Daha Fazla Göster' ile genişletilen popüler konular bölümünde gösterilecek popüler konu sayısı. " redirect_users_to_top_page: "Yeni ve uzun süredir giriş yapmamış kullanıcıları otomatik olarak Popüler sayfasına yönlendir." diff --git a/config/locales/server.zh_CN.yml b/config/locales/server.zh_CN.yml index eda4df6eb..d6be75f4a 100644 --- a/config/locales/server.zh_CN.yml +++ b/config/locales/server.zh_CN.yml @@ -736,7 +736,6 @@ zh_CN: suppress_reply_directly_above: "当一个帖子只有一个回复时,不显示回复到该贴的回复。" suppress_reply_when_quoting: "当帖子引用回复时,不显示可展开的回复到某贴的标记。" max_reply_history: "扩展回复至时显示的最大回复数量" - experimental_reply_expansion: "当展开回复至内容时隐藏直接回复(实验性)" topics_per_period_in_top_summary: "在一个主题底部显示的默认推荐主题的数量。" topics_per_period_in_top_page: "在展开“显示更多”推荐主题列表显示的主题数量。" redirect_users_to_top_page: "自动重定向至新用户或者长时间未登入的用户至热门页面。" diff --git a/config/locales/server.zh_TW.yml b/config/locales/server.zh_TW.yml index 04ee6a797..2a93ce012 100644 --- a/config/locales/server.zh_TW.yml +++ b/config/locales/server.zh_TW.yml @@ -588,7 +588,6 @@ zh_TW: suppress_reply_directly_above: "當帖子只有一個回覆時,不顯示回覆到該帖的回覆。" suppress_reply_when_quoting: "當帖子引用回覆時,不顯示可展開的回覆到某帖的標記。" max_reply_history: "擴展回覆到某帖時顯示的最大回覆數量" - experimental_reply_expansion: "當展開回覆到某帖時隱藏直接回覆(實驗性)" topics_per_period_in_top_summary: "預設推薦話題的顯示數量" topics_per_period_in_top_page: "在展開 \"顯示更多\" 推薦話題列表的顯示數量" redirect_users_to_top_page: "將新用戶或長時間未使用的用戶自動重新導向至熱門頁面" diff --git a/config/site_settings.yml b/config/site_settings.yml index 6937a8de3..b95667162 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -430,9 +430,6 @@ posting: max_reply_history: default: 1 client: true - experimental_reply_expansion: - default: false - client: true post_undo_action_window_mins: 10 max_mentions_per_post: 10 max_users_notified_per_group_mention: 100 diff --git a/lib/es6_module_transpiler/tilt/es6_module_transpiler_template.rb b/lib/es6_module_transpiler/tilt/es6_module_transpiler_template.rb index 169985693..d54524fe6 100644 --- a/lib/es6_module_transpiler/tilt/es6_module_transpiler_template.rb +++ b/lib/es6_module_transpiler/tilt/es6_module_transpiler_template.rb @@ -143,7 +143,7 @@ module Tilt def generate_source(scope) js_source = ::JSON.generate(data, quirks_mode: true) - js_source = "babel.transform(#{js_source}, {ast: false, whitelist: ['es6.constants', 'es6.properties.shorthand', 'es6.arrowFunctions', 'es6.blockScoping', 'es6.destructuring', 'es6.spread', 'es6.parameters', 'es6.templateLiterals', 'es6.regex.unicode', 'es7.decorators']})['code']" + js_source = "babel.transform(#{js_source}, {ast: false, whitelist: ['es6.constants', 'es6.properties.shorthand', 'es6.arrowFunctions', 'es6.blockScoping', 'es6.destructuring', 'es6.spread', 'es6.parameters', 'es6.templateLiterals', 'es6.regex.unicode', 'es7.decorators', 'es6.classes']})['code']" "new module.exports.Compiler(#{js_source}, '#{module_name(scope.root_path, scope.logical_path)}', #{compiler_options}).#{compiler_method}()" end diff --git a/test/javascripts/acceptance/composer-test.js.es6 b/test/javascripts/acceptance/composer-test.js.es6 index e8d8e8dbf..38789579c 100644 --- a/test/javascripts/acceptance/composer-test.js.es6 +++ b/test/javascripts/acceptance/composer-test.js.es6 @@ -192,8 +192,8 @@ test("Edit the first post", () => { ok(!exists('.topic-post:eq(0) .post-info.edits'), 'it has no edits icon at first'); - click('.topic-post:eq(0) button[data-action=showMoreActions]'); - click('.topic-post:eq(0) button[data-action=edit]'); + click('.topic-post:eq(0) button.show-more-actions'); + click('.topic-post:eq(0) button.edit'); andThen(() => { equal(find('.d-editor-input').val().indexOf('Any plans to support'), 0, 'it populates the input with the post text'); }); @@ -212,11 +212,11 @@ test("Edit the first post", () => { test("Composer can switch between edits", () => { visit("/t/this-is-a-test-topic/9"); - click('.topic-post:eq(0) button[data-action=edit]'); + click('.topic-post:eq(0) button.edit'); andThen(() => { equal(find('.d-editor-input').val().indexOf('This is the first post.'), 0, 'it populates the input with the post text'); }); - click('.topic-post:eq(1) button[data-action=edit]'); + click('.topic-post:eq(1) button.edit'); andThen(() => { equal(find('.d-editor-input').val().indexOf('This is the second post.'), 0, 'it populates the input with the post text'); }); @@ -225,9 +225,9 @@ test("Composer can switch between edits", () => { test("Composer with dirty edit can toggle to another edit", () => { visit("/t/this-is-a-test-topic/9"); - click('.topic-post:eq(0) button[data-action=edit]'); + click('.topic-post:eq(0) button.edit'); fillIn('.d-editor-input', 'This is a dirty reply'); - click('.topic-post:eq(1) button[data-action=edit]'); + click('.topic-post:eq(1) button.edit'); andThen(() => { ok(exists('.bootbox.modal'), 'it pops up a confirmation dialog'); }); @@ -240,15 +240,15 @@ test("Composer with dirty edit can toggle to another edit", () => { test("Composer can toggle between edit and reply", () => { visit("/t/this-is-a-test-topic/9"); - click('.topic-post:eq(0) button[data-action=edit]'); + click('.topic-post:eq(0) button.edit'); andThen(() => { equal(find('.d-editor-input').val().indexOf('This is the first post.'), 0, 'it populates the input with the post text'); }); - click('.topic-post:eq(0) button[data-action=reply]'); + click('.topic-post:eq(0) button.reply'); andThen(() => { equal(find('.d-editor-input').val(), "", 'it clears the input'); }); - click('.topic-post:eq(0) button[data-action=edit]'); + click('.topic-post:eq(0) button.edit'); andThen(() => { equal(find('.d-editor-input').val().indexOf('This is the first post.'), 0, 'it populates the input with the post text'); }); @@ -257,9 +257,9 @@ test("Composer can toggle between edit and reply", () => { test("Composer with dirty reply can toggle to edit", () => { visit("/t/this-is-a-test-topic/9"); - click('.topic-post:eq(0) button[data-action=reply]'); + click('.topic-post:eq(0) button.reply'); fillIn('.d-editor-input', 'This is a dirty reply'); - click('.topic-post:eq(0) button[data-action=edit]'); + click('.topic-post:eq(0) button.edit'); andThen(() => { ok(exists('.bootbox.modal'), 'it pops up a confirmation dialog'); }); @@ -272,10 +272,10 @@ test("Composer with dirty reply can toggle to edit", () => { test("Composer draft with dirty reply can toggle to edit", () => { visit("/t/this-is-a-test-topic/9"); - click('.topic-post:eq(0) button[data-action=reply]'); + click('.topic-post:eq(0) button.reply'); fillIn('.d-editor-input', 'This is a dirty reply'); click('.toggler'); - click('.topic-post:eq(0) button[data-action=edit]'); + click('.topic-post:eq(0) button.edit'); andThen(() => { ok(exists('.bootbox.modal'), 'it pops up a confirmation dialog'); }); diff --git a/test/javascripts/components/post-menu-test.js.es6 b/test/javascripts/components/post-menu-test.js.es6 deleted file mode 100644 index a027bb660..000000000 --- a/test/javascripts/components/post-menu-test.js.es6 +++ /dev/null @@ -1,52 +0,0 @@ -import componentTest from 'helpers/component-test'; - -moduleForComponent('post-menu', {integration: true}); - -function setup(store) { - const topic = store.createRecord('topic', {id: 123}); - const post = store.createRecord('post', { - id: 1, - post_number: 1, - topic, - like_count: 3, - actions_summary: [ - {id: 2, count: 3, hidden: false, can_act: true} - ] - }); - - this.on('toggleLike', function() { - post.toggleProperty('likeAction.acted'); - }); - - this.set('post', post); -} - -componentTest('basic render', { - template: '{{post-menu post=post}}', - setup, - test(assert) { - assert.ok(!!this.$('.post-menu-area').length, 'it renders a post menu'); - assert.ok(!!this.$('.actions button[data-share-url]').length, 'it renders a share button'); - } -}); - -componentTest('liking', { - template: '{{post-menu post=post toggleLike="toggleLike"}}', - setup, - test(assert) { - assert.ok(!!this.$('.actions button.like').length); - assert.ok(!!this.$('.actions button.like-count').length); - - click('.actions button.like'); - andThen(() => { - assert.ok(!this.$('.actions button.like').length); - assert.ok(!!this.$('.actions button.has-like').length); - }); - - click('.actions button.has-like'); - andThen(() => { - assert.ok(!!this.$('.actions button.like').length); - assert.ok(!this.$('.actions button.has-like').length); - }); - } -}); diff --git a/test/javascripts/controllers/topic-test.js.es6 b/test/javascripts/controllers/topic-test.js.es6 index 13fdc144e..48f0bcdbd 100644 --- a/test/javascripts/controllers/topic-test.js.es6 +++ b/test/javascripts/controllers/topic-test.js.es6 @@ -6,6 +6,7 @@ moduleFor('controller:topic', 'controller:topic', { }); import Topic from 'discourse/models/topic'; +import AppEvents from 'discourse/lib/app-events'; var buildTopic = function() { return Topic.create({ @@ -62,7 +63,7 @@ test("toggledSelectedPost", function() { }); test("selectAll", function() { - var tc = this.subject({model: buildTopic()}), + var tc = this.subject({model: buildTopic(), appEvents: AppEvents.create()}), post = Discourse.Post.create({id: 123, post_number: 2}), postStream = tc.get('model.postStream'); diff --git a/test/javascripts/fixtures/site-fixtures.js.es6 b/test/javascripts/fixtures/site-fixtures.js.es6 index 9e29ee832..81ce4b869 100644 --- a/test/javascripts/fixtures/site-fixtures.js.es6 +++ b/test/javascripts/fixtures/site-fixtures.js.es6 @@ -19,7 +19,8 @@ export default { "post_types":{ "regular":1, "moderator_action":2, - "small_action":3 + "small_action":3, + "whisper":4 }, "group_names":[ "admins", diff --git a/test/javascripts/helpers/component-test.js.es6 b/test/javascripts/helpers/component-test.js.es6 index 3ff28d3c9..cda11724b 100644 --- a/test/javascripts/helpers/component-test.js.es6 +++ b/test/javascripts/helpers/component-test.js.es6 @@ -6,21 +6,27 @@ export default function(name, opts) { opts = opts || {}; test(name, function(assert) { - if (opts.setup) { - const store = createStore(); - opts.setup.call(this, store); - } const appEvents = AppEvents.create(); - - loadAllHelpers(); - this.container.register('site-settings:main', Discourse.SiteSettings, { instantiate: false }); this.container.register('app-events:main', appEvents, { instantiate: false }); this.container.register('capabilities:main', Ember.Object); + this.container.register('site:main', Discourse.Site.current(), { instantiate: false }); this.container.injection('component', 'siteSettings', 'site-settings:main'); this.container.injection('component', 'appEvents', 'app-events:main'); this.container.injection('component', 'capabilities', 'capabilities:main'); + this.siteSettings = Discourse.SiteSettings; + + loadAllHelpers(); + + if (opts.setup) { + const store = createStore(); + this.currentUser = Discourse.User.create(); + this.container.register('store:main', store, { instantiate: false }); + this.container.register('current-user:main', this.currentUser, { instantiate: false }); + opts.setup.call(this, store); + } + andThen(() => this.render(opts.template)); andThen(() => opts.test.call(this, assert)); }); diff --git a/test/javascripts/helpers/create-pretender.js.es6 b/test/javascripts/helpers/create-pretender.js.es6 index fca63e935..0aaad1ff9 100644 --- a/test/javascripts/helpers/create-pretender.js.es6 +++ b/test/javascripts/helpers/create-pretender.js.es6 @@ -85,7 +85,23 @@ export default function() { this.get('/users/:username/staff-info.json', () => response({})); - this.put('/categories/:category_id', function(request) { + this.get('/post_action_users', () => { + return response({ + post_action_users: [ + {id: 1, username: 'eviltrout', avatar_template: '/user_avatar/default/eviltrout/{size}/1.png', username_lower: 'eviltrout' } + ] + }); + }); + + this.get('/post_replies', () => { + return response({ post_replies: [{ id: 1234, cooked: 'wat' }] }); + }); + + this.get('/post_reply_histories', () => { + return response({ post_reply_histories: [{ id: 1234, cooked: 'wat' }] }); + }); + + this.put('/categories/:category_id', request => { const category = parsePostData(request.requestBody); return response({category}); }); @@ -132,6 +148,7 @@ export default function() { this.delete('/posts/:post_id', success); this.put('/posts/:post_id/recover', success); + this.get('/posts/:post_id/expand-embed', success); this.put('/posts/:post_id', request => { const data = parsePostData(request.requestBody); diff --git a/test/javascripts/helpers/qunit-helpers.js.es6 b/test/javascripts/helpers/qunit-helpers.js.es6 index 44812f7ec..c69058566 100644 --- a/test/javascripts/helpers/qunit-helpers.js.es6 +++ b/test/javascripts/helpers/qunit-helpers.js.es6 @@ -41,7 +41,6 @@ function acceptance(name, options) { Discourse.Utilities.avatarImg = () => ""; // For now don't do scrolling stuff in Test Mode - Ember.CloakedCollectionView.scrolled = Ember.K; HeaderView.reopen({examineDockHeader: Ember.K}); var siteJson = siteFixtures['site.json'].site; diff --git a/test/javascripts/helpers/widget-test.js.es6 b/test/javascripts/helpers/widget-test.js.es6 new file mode 100644 index 000000000..d30170ff1 --- /dev/null +++ b/test/javascripts/helpers/widget-test.js.es6 @@ -0,0 +1,9 @@ +import componentTest from 'helpers/component-test'; + +export function moduleForWidget(name) { + moduleForComponent(name, `widget:${name}`, { integration: true }); +} + +export function widgetTest(name, opts) { + return componentTest(name, opts); +} diff --git a/test/javascripts/models/post-stream-test.js.es6 b/test/javascripts/models/post-stream-test.js.es6 index 41098560a..0b9a7d3df 100644 --- a/test/javascripts/models/post-stream-test.js.es6 +++ b/test/javascripts/models/post-stream-test.js.es6 @@ -27,40 +27,6 @@ test('defaults', function() { present(postStream.get('topic')); }); -test('daysSincePrevious when appending', function(assert) { - const postStream = buildStream(10000001, [1,2,3]); - const store = postStream.store; - - const p1 = store.createRecord('post', {id: 1, post_number: 1, created_at: "2015-05-29T18:17:35.868Z"}), - p2 = store.createRecord('post', {id: 2, post_number: 2, created_at: "2015-06-01T01:07:25.761Z"}), - p3 = store.createRecord('post', {id: 3, post_number: 3, created_at: "2015-06-02T01:07:25.761Z"}); - - postStream.appendPost(p1); - postStream.appendPost(p2); - postStream.appendPost(p3); - - assert.ok(!p1.get('daysSincePrevious')); - assert.equal(p2.get('daysSincePrevious'), 2); - assert.equal(p3.get('daysSincePrevious'), 1); -}); - -test('daysSincePrevious when prepending', function(assert) { - const postStream = buildStream(10000001, [1,2,3]); - const store = postStream.store; - - const p1 = store.createRecord('post', {id: 1, post_number: 1, created_at: "2015-05-29T18:17:35.868Z"}), - p2 = store.createRecord('post', {id: 2, post_number: 2, created_at: "2015-06-01T01:07:25.761Z"}), - p3 = store.createRecord('post', {id: 3, post_number: 3, created_at: "2015-06-02T01:07:25.761Z"}); - - postStream.prependPost(p3); - postStream.prependPost(p2); - postStream.prependPost(p1); - - assert.ok(!p1.get('daysSincePrevious')); - assert.equal(p2.get('daysSincePrevious'), 2); - assert.equal(p3.get('daysSincePrevious'), 1); -}); - test('appending posts', function() { const postStream = buildStream(4567, [1, 3, 4]); const store = postStream.store; @@ -320,17 +286,6 @@ test("loadIntoIdentityMap with post ids", function() { }); }); -test("loading a post's history", function() { - const postStream = buildStream(1234); - const store = postStream.store; - const post = store.createRecord('post', {id: 4321}); - - postStream.findReplyHistory(post).then(function() { - present(postStream.findLoadedPost(2222), "it stores the returned post in the identity map"); - present(post.get('replyHistory'), "it sets the replyHistory attribute for the post"); - }); -}); - test("staging and undoing a new post", function() { const postStream = buildStream(10101, [1]); const store = postStream.store; diff --git a/test/javascripts/models/post-test.js.es6 b/test/javascripts/models/post-test.js.es6 index 773657750..1f881df12 100644 --- a/test/javascripts/models/post-test.js.es6 +++ b/test/javascripts/models/post-test.js.es6 @@ -14,7 +14,6 @@ test('defaults', function() { var post = Discourse.Post.create({id: 1}); blank(post.get('deleted_at'), "it has no deleted_at by default"); blank(post.get('deleted_by'), "there is no deleted_by by default"); - equal(post.get('replyHistory.length'), 0, "there is no reply history by default"); }); test('new_user', function() { @@ -47,16 +46,6 @@ test('updateFromPost', function() { equal(post.get('raw'), "different raw", "raw field updated"); }); -test('hasHistory', function() { - var post = Discourse.Post.create({id: 1}); - ok(!post.get('hasHistory'), 'posts without versions have no history'); - post.set('version', 1); - ok(!post.get('hasHistory'), 'posts with one version have no history'); - post.set('version', 2); - ok(post.get('hasHistory'), 'posts with more than one version have a history'); -}); - - test('destroy by staff', function() { var user = Discourse.User.create({username: 'staff', staff: true}), post = buildPost({user: user}); diff --git a/test/javascripts/test_helper.js b/test/javascripts/test_helper.js index 08f9256f6..d8ca4fe86 100644 --- a/test/javascripts/test_helper.js +++ b/test/javascripts/test_helper.js @@ -111,9 +111,6 @@ QUnit.testStart(function(ctx) { } }); -// Don't cloak in testing -Ember.CloakedCollectionView = Ember.CollectionView; - QUnit.testDone(function() { Ember.run.debounce = origDebounce; window.sandbox.restore(); diff --git a/test/javascripts/widgets/actions-summary-test.js.es6 b/test/javascripts/widgets/actions-summary-test.js.es6 new file mode 100644 index 000000000..17cb172c6 --- /dev/null +++ b/test/javascripts/widgets/actions-summary-test.js.es6 @@ -0,0 +1,80 @@ +import { moduleForWidget, widgetTest } from 'helpers/widget-test'; + +moduleForWidget('actions-summary'); + +widgetTest('listing actions', { + template: '{{mount-widget widget="actions-summary" args=args}}', + setup() { + this.set('args', { + actionsSummary: [ + {action: 'off_topic', description: 'very off topic'}, + {action: 'spam', description: 'suspicious message'} + ] + }); + }, + test(assert) { + assert.equal(this.$('.post-actions .post-action').length, 2); + + click('.post-action:eq(0) .action-link a'); + andThen(() => { + assert.equal(this.$('.post-action:eq(0) img.avatar').length, 1, 'clicking it shows the user'); + }); + } +}); + +widgetTest('undo', { + template: '{{mount-widget widget="actions-summary" args=args undoPostAction="undoPostAction"}}', + setup() { + this.set('args', { + actionsSummary: [ + {action: 'off_topic', description: 'very off topic', canUndo: true}, + ] + }); + + this.on('undoPostAction', () => this.undid = true); + }, + test(assert) { + assert.equal(this.$('.post-actions .post-action').length, 1); + + click('.action-link.undo'); + andThen(() => { + assert.ok(this.undid, 'it triggered the action'); + }); + } +}); + +widgetTest('deferFlags', { + template: '{{mount-widget widget="actions-summary" args=args deferPostActionFlags="deferPostActionFlags"}}', + setup() { + this.set('args', { + actionsSummary: [ + {action: 'off_topic', description: 'very off topic', canDeferFlags: true, count: 1}, + ] + }); + + this.on('deferPostActionFlags', () => this.deferred = true); + }, + test(assert) { + assert.equal(this.$('.post-actions .post-action').length, 1); + + click('.action-link.defer-flags'); + andThen(() => { + assert.ok(this.deferred, 'it triggered the action'); + }); + } +}); + +widgetTest('post deleted', { + template: '{{mount-widget widget="actions-summary" args=args}}', + setup() { + this.set('args', { + isDeleted: true, + deletedByUsername: 'eviltrout', + deletedByAvatarTemplate: '/images/avatar.png' + }); + }, + test(assert) { + assert.ok(this.$('.post-action .fa-trash-o').length === 1, 'it has the deleted icon'); + assert.ok(this.$('.avatar[title=eviltrout]').length === 1, 'it has the deleted by avatar'); + } +}); diff --git a/test/javascripts/widgets/post-gutter-test.js.es6 b/test/javascripts/widgets/post-gutter-test.js.es6 new file mode 100644 index 000000000..55c4152cb --- /dev/null +++ b/test/javascripts/widgets/post-gutter-test.js.es6 @@ -0,0 +1,54 @@ +import { moduleForWidget, widgetTest } from 'helpers/widget-test'; + +moduleForWidget('post-gutter'); + +widgetTest("duplicate links", { + template: '{{mount-widget widget="post-gutter" args=args}}', + setup() { + this.set('args', { + links: [ + { title: "Evil Trout Link", url: "http://eviltrout.com" }, + { title: "Evil Trout Link", url: "http://dupe.eviltrout.com" } + ] + }); + }, + test(assert) { + assert.equal(this.$('.post-links a.track-link').length, 1, 'it hides the dupe link'); + } +}); + +widgetTest("collapsed links", { + template: '{{mount-widget widget="post-gutter" args=args}}', + setup() { + this.set('args', { + links: [ + { title: "Link 1", url: "http://eviltrout.com?1" }, + { title: "Link 2", url: "http://eviltrout.com?2" }, + { title: "Link 3", url: "http://eviltrout.com?3" }, + { title: "Link 4", url: "http://eviltrout.com?4" }, + { title: "Link 5", url: "http://eviltrout.com?5" }, + { title: "Link 6", url: "http://eviltrout.com?6" }, + { title: "Link 7", url: "http://eviltrout.com?7" }, + ] + }); + }, + test(assert) { + assert.equal(this.$('.post-links a.track-link').length, 5, 'collapses by default'); + click('a.toggle-more'); + andThen(() => { + assert.equal(this.$('.post-links a.track-link').length, 7); + }); + } +}); + +widgetTest("reply as new topic", { + template: '{{mount-widget widget="post-gutter" args=args newTopicAction="newTopicAction"}}', + setup() { + this.set('args', { canReplyAsNewTopic: true }); + this.on('newTopicAction', () => this.newTopicTriggered = true); + }, + test(assert) { + click('a.reply-new'); + andThen(() => assert.ok(this.newTopicTriggered)); + } +}); diff --git a/test/javascripts/widgets/post-stream-test.js.es6 b/test/javascripts/widgets/post-stream-test.js.es6 new file mode 100644 index 000000000..62966d7b0 --- /dev/null +++ b/test/javascripts/widgets/post-stream-test.js.es6 @@ -0,0 +1,66 @@ +import { moduleForWidget, widgetTest } from 'helpers/widget-test'; +import Topic from 'discourse/models/topic'; +import Post from 'discourse/models/post'; + +moduleForWidget('post-stream'); + +function postStreamTest(name, attrs) { + widgetTest(name, { + template: `{{mount-widget widget="post-stream" args=(as-hash posts=posts)}}`, + setup() { + this.set('posts', attrs.posts.call(this)); + }, + test: attrs.test + }); +} + +postStreamTest('basics', { + posts() { + const site = this.container.lookup('site:main'); + const topic = Topic.create({ details: { created_by: { id: 123 } } }); + return [ + Post.create({ topic, id: 1, post_number: 1, user_id: 123, primary_group_name: 'trout', + avatar_template: '/images/avatar.png' }), + Post.create({ topic, id: 2, post_number: 2, post_type: site.get('post_types.moderator_action') }), + Post.create({ topic, id: 3, post_number: 3, hidden: true }), + Post.create({ topic, id: 4, post_number: 4, post_type: site.get('post_types.whisper') }), + Post.create({ topic, id: 5, post_number: 5, wiki: true, via_email: true }) + ]; + }, + + test(assert) { + assert.equal(this.$('.post-stream').length, 1); + assert.equal(this.$('.topic-post').length, 5, 'renders all posts'); + + // look for special class bindings + assert.equal(this.$('.topic-post:eq(0).topic-owner').length, 1, 'it applies the topic owner class'); + assert.equal(this.$('.topic-post:eq(0).group-trout').length, 1, 'it applies the primary group class'); + assert.equal(this.$('.topic-post:eq(0).regular').length, 1, 'it applies the regular class'); + assert.equal(this.$('.topic-post:eq(1).moderator').length, 1, 'it applies the moderator class'); + assert.equal(this.$('.topic-post:eq(2).post-hidden').length, 1, 'it applies the hidden class'); + assert.equal(this.$('.topic-post:eq(3).whisper').length, 1, 'it applies the whisper class'); + assert.equal(this.$('.topic-post:eq(4).wiki').length, 1, 'it applies the wiki class'); + + // it renders an article for the body with appropriate attributes + assert.equal(this.$('article#post_2').length, 1); + assert.equal(this.$('article[data-user-id=123]').length, 1); + assert.equal(this.$('article[data-post-id=3]').length, 1); + assert.equal(this.$('article#post_5.via-email').length, 1); + + assert.equal(this.$('article:eq(0) .main-avatar').length, 1, 'renders the main avatar'); + } +}); + +postStreamTest('deleted posts', { + posts() { + const topic = Topic.create({ details: { created_by: { id: 123 } } }); + return [ + Post.create({ topic, id: 1, post_number: 1, deleted_at: new Date().getTime() }), + ]; + }, + + test(assert) { + assert.equal(this.$('.topic-post.deleted').length, 1, 'it applies the deleted class'); + assert.equal(this.$('.deleted-user-avatar').length, 1, 'it has the trash avatar'); + } +}); diff --git a/test/javascripts/widgets/post-test.js.es6 b/test/javascripts/widgets/post-test.js.es6 new file mode 100644 index 000000000..5c853c0dd --- /dev/null +++ b/test/javascripts/widgets/post-test.js.es6 @@ -0,0 +1,784 @@ +import { moduleForWidget, widgetTest } from 'helpers/widget-test'; + +moduleForWidget('post'); + +widgetTest('basic elements', { + template: '{{mount-widget widget="post" args=args}}', + setup() { + this.set('args', { shareUrl: '/example', post_number: 1 }); + }, + test(assert) { + assert.ok(this.$('.names').length, 'includes poster name'); + + assert.ok(this.$('a.post-date').length, 'includes post date'); + assert.ok(this.$('a.post-date[data-share-url]').length); + assert.ok(this.$('a.post-date[data-post-number]').length); + } +}); + +widgetTest('wiki', { + template: '{{mount-widget widget="post" args=args editPost="editPost"}}', + setup() { + this.set('args', { wiki: true }); + this.on('editPost', () => this.editPostCalled = true); + }, + test(assert) { + click('.post-info.wiki'); + andThen(() => { + assert.ok(this.editPostCalled, 'clicking the wiki icon edits the post'); + }); + } +}); + +widgetTest('via-email', { + template: '{{mount-widget widget="post" args=args showRawEmail="showRawEmail"}}', + setup() { + this.set('args', { via_email: true, canViewRawEmail: true }); + this.on('showRawEmail', () => this.rawEmailShown = true); + }, + test(assert) { + click('.post-info.via-email'); + andThen(() => { + assert.ok(this.rawEmailShown, 'clicking the enveloppe shows the raw email'); + }); + } +}); + +widgetTest('via-email without permission', { + template: '{{mount-widget widget="post" args=args showRawEmail="showRawEmail"}}', + setup() { + this.set('args', { via_email: true, canViewRawEmail: false }); + this.on('showRawEmail', () => this.rawEmailShown = true); + }, + test(assert) { + click('.post-info.via-email'); + andThen(() => { + assert.ok(!this.rawEmailShown, `clicking the enveloppe doesn't show the raw email`); + }); + } +}); + +widgetTest('history', { + template: '{{mount-widget widget="post" args=args showHistory="showHistory"}}', + setup() { + this.set('args', { version: 3, canViewEditHistory: true }); + this.on('showHistory', () => this.historyShown = true); + }, + test(assert) { + click('.post-info.edits'); + andThen(() => { + assert.ok(this.historyShown, 'clicking the pencil shows the history'); + }); + } +}); + +widgetTest('history without view permission', { + template: '{{mount-widget widget="post" args=args showHistory="showHistory"}}', + setup() { + this.set('args', { version: 3, canViewEditHistory: false }); + this.on('showHistory', () => this.historyShown = true); + }, + test(assert) { + click('.post-info.edits'); + andThen(() => { + assert.ok(!this.historyShown, `clicking the pencil doesn't show the history`); + }); + } +}); + +widgetTest('whisper', { + template: '{{mount-widget widget="post" args=args}}', + setup() { + this.set('args', { isWhisper: true }); + }, + test(assert) { + assert.ok(this.$('.topic-post.whisper').length === 1); + assert.ok(this.$('.post-info.whisper').length === 1); + } +}); + +widgetTest('like count button', { + template: '{{mount-widget widget="post" model=post args=args}}', + setup(store) { + const topic = store.createRecord('topic', {id: 123}); + const post = store.createRecord('post', { + id: 1, + post_number: 1, + topic, + like_count: 3, + actions_summary: [ {id: 2, count: 1, hidden: false, can_act: true} ] + }); + this.set('post', post); + this.set('args', { likeCount: 1 }); + }, + test(assert) { + assert.ok(this.$('button.like-count').length === 1); + assert.ok(this.$('.who-liked').length === 0); + + // toggle it on + click('button.like-count'); + andThen(() => { + assert.ok(this.$('.who-liked').length === 1); + assert.ok(this.$('.who-liked a.trigger-user-card').length === 1); + }); + + // toggle it off + click('button.like-count'); + andThen(() => { + assert.ok(this.$('.who-liked').length === 0); + assert.ok(this.$('.who-liked a.trigger-user-card').length === 0); + }); + } +}); + +widgetTest(`like count with no likes`, { + template: '{{mount-widget widget="post" model=post args=args}}', + setup() { + this.set('args', { likeCount: 0 }); + }, + test(assert) { + assert.ok(this.$('button.like-count').length === 0); + } +}); + +widgetTest('share button', { + template: '{{mount-widget widget="post" args=args}}', + setup() { + this.set('args', { shareUrl: 'http://share-me.example.com' }); + }, + test(assert) { + assert.ok(!!this.$('.actions button[data-share-url]').length, 'it renders a share button'); + } +}); + +widgetTest('liking', { + template: '{{mount-widget widget="post-menu" args=args toggleLike="toggleLike"}}', + setup() { + const args = { showLike: true, canToggleLike: true }; + this.set('args', args); + this.on('toggleLike', () => { + args.liked = !args.liked; + args.likeCount = args.liked ? 1 : 0; + }); + }, + test(assert) { + assert.ok(!!this.$('.actions button.like').length); + assert.ok(this.$('.actions button.like-count').length === 0); + + click('.actions button.like'); + andThen(() => { + assert.ok(!this.$('.actions button.like').length); + assert.ok(!!this.$('.actions button.has-like').length); + assert.ok(this.$('.actions button.like-count').length === 1); + }); + + click('.actions button.has-like'); + andThen(() => { + assert.ok(!!this.$('.actions button.like').length); + assert.ok(!this.$('.actions button.has-like').length); + assert.ok(this.$('.actions button.like-count').length === 0); + }); + } +}); + +widgetTest('edit button', { + template: '{{mount-widget widget="post" args=args editPost="editPost"}}', + setup() { + this.set('args', { canEdit: true }); + this.on('editPost', () => this.editPostCalled = true); + }, + test(assert) { + click('button.edit'); + andThen(() => { + assert.ok(this.editPostCalled, 'it triggered the edit action'); + }); + } +}); + +widgetTest(`edit button - can't edit`, { + template: '{{mount-widget widget="post" args=args editPost="editPost"}}', + setup() { + this.set('args', { canEdit: false }); + }, + test(assert) { + assert.equal(this.$('button.edit').length, 0, `button is not displayed`); + } +}); + +widgetTest('recover button', { + template: '{{mount-widget widget="post" args=args deletePost="deletePost"}}', + setup() { + this.set('args', { canDelete: true }); + this.on('deletePost', () => this.deletePostCalled = true); + }, + test(assert) { + click('button.delete'); + andThen(() => { + assert.ok(this.deletePostCalled, 'it triggered the delete action'); + }); + } +}); + +widgetTest('delete topic button', { + template: '{{mount-widget widget="post" args=args deletePost="deletePost"}}', + setup() { + this.set('args', { canDeleteTopic: true }); + this.on('deletePost', () => this.deletePostCalled = true); + }, + test(assert) { + click('button.delete'); + andThen(() => { + assert.ok(this.deletePostCalled, 'it triggered the delete action'); + }); + } +}); + +widgetTest(`delete topic button - can't delete`, { + template: '{{mount-widget widget="post" args=args deletePost="deletePost"}}', + setup() { + this.set('args', { canDeleteTopic: false }); + }, + test(assert) { + assert.equal(this.$('button.delete').length, 0, `button is not displayed`); + } +}); + +widgetTest('recover topic button', { + template: '{{mount-widget widget="post" args=args recoverPost="recoverPost"}}', + setup() { + this.set('args', { canRecoverTopic: true }); + this.on('recoverPost', () => this.recovered = true); + }, + test(assert) { + click('button.recover'); + andThen(() => assert.ok(this.recovered)); + } +}); + +widgetTest(`recover topic button - can't recover`, { + template: '{{mount-widget widget="post" args=args deletePost="deletePost"}}', + setup() { + this.set('args', { canRecoverTopic: false }); + }, + test(assert) { + assert.equal(this.$('button.recover').length, 0, `button is not displayed`); + } +}); + +widgetTest('delete post button', { + template: '{{mount-widget widget="post" args=args deletePost="deletePost"}}', + setup() { + this.set('args', { canDelete: true }); + this.on('deletePost', () => this.deletePostCalled = true); + }, + test(assert) { + click('button.delete'); + andThen(() => { + assert.ok(this.deletePostCalled, 'it triggered the delete action'); + }); + } +}); + +widgetTest(`delete post button - can't delete`, { + template: '{{mount-widget widget="post" args=args}}', + setup() { + this.set('args', { canDelete: false }); + }, + test(assert) { + assert.equal(this.$('button.delete').length, 0, `button is not displayed`); + } +}); + +widgetTest('recover post button', { + template: '{{mount-widget widget="post" args=args recoverPost="recoverPost"}}', + setup() { + this.set('args', { canRecover: true }); + this.on('recoverPost', () => this.recovered = true); + }, + test(assert) { + click('button.recover'); + andThen(() => assert.ok(this.recovered)); + } +}); + +widgetTest(`recover post button - can't recover`, { + template: '{{mount-widget widget="post" args=args deletePost="deletePost"}}', + setup() { + this.set('args', { canRecover: false }); + }, + test(assert) { + assert.equal(this.$('button.recover').length, 0, `button is not displayed`); + } +}); + +widgetTest(`flagging`, { + template: '{{mount-widget widget="post" args=args showFlags="showFlags"}}', + setup() { + this.set('args', { canFlag: true }); + this.on('showFlags', () => this.flagsShown = true); + }, + test(assert) { + assert.ok(this.$('button.create-flag').length === 1); + + click('button.create-flag'); + andThen(() => { + assert.ok(this.flagsShown, 'it triggered the action'); + }); + } +}); + +widgetTest(`flagging: can't flag`, { + template: '{{mount-widget widget="post" args=args}}', + setup() { + this.set('args', { canFlag: false }); + }, + test(assert) { + assert.ok(this.$('button.create-flag').length === 0); + } +}); + +widgetTest(`read indicator`, { + template: '{{mount-widget widget="post" args=args}}', + setup() { + this.set('args', { read: true }); + }, + test(assert) { + assert.ok(this.$('.read-state.read').length); + } +}); + +widgetTest(`unread indicator`, { + template: '{{mount-widget widget="post" args=args}}', + setup() { + this.set('args', { read: false }); + }, + test(assert) { + assert.ok(this.$('.read-state').length); + } +}); + +widgetTest("reply directly above (supressed)", { + template: '{{mount-widget widget="post" args=args}}', + setup() { + this.set('args', { + replyToUsername: 'eviltrout', + replyToAvatarTemplate: '/images/avatar.png', + replyDirectlyAbove: true + }); + }, + test(assert) { + assert.equal(this.$('a.reply-to-tab').length, 0, 'hides the tab'); + assert.equal(this.$('.avoid-tab').length, 0, "doesn't have the avoid tab class"); + } +}); + +widgetTest("reply a few posts above (supressed)", { + template: '{{mount-widget widget="post" args=args}}', + setup() { + this.set('args', { + replyToUsername: 'eviltrout', + replyToAvatarTemplate: '/images/avatar.png', + replyDirectlyAbove: false + }); + }, + test(assert) { + assert.ok(this.$('a.reply-to-tab').length, 'shows the tab'); + assert.equal(this.$('.avoid-tab').length, 1, "has the avoid tab class"); + } +}); + +widgetTest("reply directly above", { + template: '{{mount-widget widget="post" args=args}}', + setup() { + this.set('args', { + replyToUsername: 'eviltrout', + replyToAvatarTemplate: '/images/avatar.png', + replyDirectlyAbove: true + }); + this.siteSettings.suppress_reply_directly_above = false; + }, + test(assert) { + assert.equal(this.$('.avoid-tab').length, 1, "has the avoid tab class"); + click('a.reply-to-tab'); + andThen(() => { + assert.equal(this.$('section.embedded-posts.top .cooked').length, 1); + assert.equal(this.$('section.embedded-posts i.fa-arrow-up').length, 1); + }); + } +}); + +widgetTest("cooked content hidden", { + template: '{{mount-widget widget="post" args=args expandHidden="expandHidden"}}', + setup() { + this.set('args', { cooked_hidden: true }); + this.on('expandHidden', () => this.unhidden = true); + }, + test(assert) { + click('.topic-body .expand-hidden'); + andThen(() => { + assert.ok(this.unhidden, 'triggers the action'); + }); + } +}); + +widgetTest("expand first post", { + template: '{{mount-widget widget="post" model=post args=args}}', + setup(store) { + this.set('args', { expandablePost: true }); + this.set('post', store.createRecord('post', { id: 1234 })); + }, + test(assert) { + click('.topic-body .expand-post'); + andThen(() => { + assert.equal(this.$('.expand-post').length, 0, 'button is gone'); + }); + } +}); + +widgetTest("can't bookmark", { + template: '{{mount-widget widget="post" args=args}}', + setup() { + this.set('args', { canBookmark: false }); + }, + test(assert) { + assert.equal(this.$('button.bookmark').length, 0); + assert.equal(this.$('button.bookmarked').length, 0); + } +}); + +widgetTest("bookmark", { + template: '{{mount-widget widget="post" args=args toggleBookmark="toggleBookmark"}}', + setup() { + const args = { canBookmark: true }; + + this.set('args', args); + this.on('toggleBookmark', () => args.bookmarked = true); + }, + test(assert) { + assert.equal(this.$('.post-menu-area .bookmark').length, 1); + assert.equal(this.$('button.bookmarked').length, 0); + + click('button.bookmark'); + andThen(() => { + assert.equal(this.$('button.bookmarked').length, 1); + }); + } +}); + +widgetTest("can't show admin menu when you can't manage", { + template: '{{mount-widget widget="post" args=args}}', + setup() { + this.set('args', { canManage: false }); + }, + test(assert) { + assert.equal(this.$('.post-menu-area .show-post-admin-menu').length, 0); + } +}); + +widgetTest("show admin menu", { + template: '{{mount-widget widget="post" args=args}}', + setup() { + this.set('args', { canManage: true }); + }, + test(assert) { + assert.equal(this.$('.post-admin-menu').length, 0); + click('.post-menu-area .show-post-admin-menu'); + andThen(() => { + assert.equal(this.$('.post-admin-menu').length, 1, 'it shows the popup'); + }); + click('.post-menu-area'); + andThen(() => { + assert.equal(this.$('.post-admin-menu').length, 0, 'clicking outside clears the popup'); + }); + } +}); + +widgetTest("toggle moderator post", { + template: '{{mount-widget widget="post" args=args togglePostType="togglePostType"}}', + setup() { + this.set('args', { canManage: true }); + this.on('togglePostType', () => this.toggled = true); + }, + test(assert) { + click('.post-menu-area .show-post-admin-menu'); + click('.post-admin-menu .toggle-post-type'); + andThen(() => { + assert.ok(this.toggled); + assert.equal(this.$('.post-admin-menu').length, 0, 'also hides the menu'); + }); + } +}); +widgetTest("toggle moderator post", { + template: '{{mount-widget widget="post" args=args togglePostType="togglePostType"}}', + setup() { + this.set('args', { canManage: true }); + this.on('togglePostType', () => this.toggled = true); + }, + test(assert) { + click('.post-menu-area .show-post-admin-menu'); + click('.post-admin-menu .toggle-post-type'); + andThen(() => { + assert.ok(this.toggled); + assert.equal(this.$('.post-admin-menu').length, 0, 'also hides the menu'); + }); + } +}); + +widgetTest("rebake post", { + template: '{{mount-widget widget="post" args=args rebakePost="rebakePost"}}', + setup() { + this.set('args', { canManage: true }); + this.on('rebakePost', () => this.baked = true); + }, + test(assert) { + click('.post-menu-area .show-post-admin-menu'); + click('.post-admin-menu .rebuild-html'); + andThen(() => { + assert.ok(this.baked); + assert.equal(this.$('.post-admin-menu').length, 0, 'also hides the menu'); + }); + } +}); + +widgetTest("unhide post", { + template: '{{mount-widget widget="post" args=args unhidePost="unhidePost"}}', + setup() { + this.set('args', { canManage: true, hidden: true }); + this.on('unhidePost', () => this.unhidden = true); + }, + test(assert) { + click('.post-menu-area .show-post-admin-menu'); + click('.post-admin-menu .unhide-post'); + andThen(() => { + assert.ok(this.unhidden); + assert.equal(this.$('.post-admin-menu').length, 0, 'also hides the menu'); + }); + } +}); + +widgetTest("change owner", { + template: '{{mount-widget widget="post" args=args changePostOwner="changePostOwner"}}', + setup() { + this.currentUser.admin = true; + this.set('args', { canManage: true }); + this.on('changePostOwner', () => this.owned = true); + }, + test(assert) { + click('.post-menu-area .show-post-admin-menu'); + click('.post-admin-menu .change-owner'); + andThen(() => { + assert.ok(this.owned); + assert.equal(this.$('.post-admin-menu').length, 0, 'also hides the menu'); + }); + } +}); + +widgetTest("reply", { + template: '{{mount-widget widget="post" args=args replyToPost="replyToPost"}}', + setup() { + this.set('args', { canCreatePost: true }); + this.on('replyToPost', () => this.replied = true); + }, + test(assert) { + click('.post-controls .create'); + andThen(() => { + assert.ok(this.replied); + }); + } +}); + +widgetTest("reply - without permissions", { + template: '{{mount-widget widget="post" args=args}}', + setup() { + this.set('args', { canCreatePost: false }); + }, + test(assert) { + assert.equal(this.$('.post-controls .create').length, 0); + } +}); + +widgetTest("replies - no replies", { + template: '{{mount-widget widget="post" args=args}}', + setup() { + this.set('args', {replyCount: 0}); + }, + test(assert) { + assert.equal(this.$('button.show-replies').length, 0); + } +}); + +widgetTest("replies - multiple replies", { + template: '{{mount-widget widget="post" args=args}}', + setup() { + this.siteSettings.suppress_reply_directly_below = true; + this.set('args', {replyCount: 2, replyDirectlyBelow: true}); + }, + test(assert) { + assert.equal(this.$('button.show-replies').length, 1); + } +}); + +widgetTest("replies - one below, suppressed", { + template: '{{mount-widget widget="post" args=args}}', + setup() { + this.siteSettings.suppress_reply_directly_below = true; + this.set('args', {replyCount: 1, replyDirectlyBelow: true}); + }, + test(assert) { + assert.equal(this.$('button.show-replies').length, 0); + } +}); + +widgetTest("replies - one below, not suppressed", { + template: '{{mount-widget widget="post" args=args}}', + setup() { + this.siteSettings.suppress_reply_directly_below = false; + this.set('args', {id: 6654, replyCount: 1, replyDirectlyBelow: true}); + }, + test(assert) { + click('button.show-replies'); + andThen(() => { + assert.equal(this.$('section.embedded-posts.bottom .cooked').length, 1); + assert.equal(this.$('section.embedded-posts i.fa-arrow-down').length, 1); + }); + } +}); + +widgetTest("topic map not shown", { + template: '{{mount-widget widget="post" args=args}}', + setup() { + this.set('args', { showTopicMap: false }); + }, + test(assert) { + assert.equal(this.$('.topic-map').length, 0); + } +}); + +widgetTest("topic map - few posts", { + template: '{{mount-widget widget="post" args=args}}', + setup() { + this.set('args', { + showTopicMap: true, + topicPostsCount: 2, + participants: [ + {username: 'eviltrout'}, + {username: 'codinghorror'}, + ] + }); + }, + test(assert) { + assert.equal(this.$('li.avatars a.poster').length, 0, 'shows no participants when collapsed'); + + click('nav.buttons button'); + andThen(() => { + assert.equal(this.$('.topic-map-expanded a.poster').length, 2, 'shows all when expanded'); + }); + } +}); + +widgetTest("topic map - participants", { + template: '{{mount-widget widget="post" args=args toggleParticipant="toggleParticipant"}}', + setup() { + this.set('args', { + showTopicMap: true, + topicPostsCount: 10, + participants: [ + {username: 'eviltrout'}, + {username: 'codinghorror'}, + {username: 'sam'}, + {username: 'ZogStrIP'}, + ], + userFilters: ['sam', 'codinghorror'] + }); + + this.on('toggleParticipant', () => this.participantToggled = true); + }, + test(assert) { + assert.equal(this.$('li.avatars a.poster').length, 3, 'limits to three participants'); + + click('nav.buttons button'); + andThen(() => { + assert.equal(this.$('li.avatars a.poster').length, 0); + assert.equal(this.$('.topic-map-expanded a.poster').length, 4, 'shows all when expanded'); + assert.equal(this.$('a.poster.toggled').length, 2, 'two are toggled'); + }); + + click('.topic-map-expanded a.poster:eq(0)'); + andThen(() => assert.ok(this.participantToggled)); + } +}); + +widgetTest("topic map - links", { + template: '{{mount-widget widget="post" args=args}}', + setup() { + this.set('args', { + showTopicMap: true, + topicLinks: [ + {url: 'http://link1.example.com', clicks: 0}, + {url: 'http://link2.example.com', clicks: 0}, + {url: 'http://link3.example.com', clicks: 0}, + {url: 'http://link4.example.com', clicks: 0}, + {url: 'http://link5.example.com', clicks: 0}, + {url: 'http://link6.example.com', clicks: 0}, + ] + }); + }, + test(assert) { + assert.equal(this.$('.topic-map').length, 1); + assert.equal(this.$('.map.map-collapsed').length, 1); + assert.equal(this.$('.topic-map-expanded').length, 0); + + click('nav.buttons button'); + andThen(() => { + assert.equal(this.$('.map.map-collapsed').length, 0); + assert.equal(this.$('.topic-map i.fa-chevron-up').length, 1); + assert.equal(this.$('.topic-map-expanded').length, 1); + assert.equal(this.$('.topic-map-expanded .topic-link').length, 5, 'it limits the links displayed'); + }); + + click('.link-summary a'); + andThen(() => { + assert.equal(this.$('.topic-map-expanded .topic-link').length, 6, 'all links now shown'); + }); + } +}); + +widgetTest("topic map - no summary", { + template: '{{mount-widget widget="post" args=args}}', + setup() { + this.set('args', { showTopicMap: true }); + }, + test(assert) { + assert.equal(this.$('.toggle-summary').length, 0); + } +}); + +widgetTest("topic map - has summary", { + template: '{{mount-widget widget="post" args=args toggleSummary="toggleSummary"}}', + setup() { + this.set('args', { showTopicMap: true, hasTopicSummary: true }); + this.on('toggleSummary', () => this.summaryToggled = true); + }, + test(assert) { + assert.equal(this.$('.toggle-summary').length, 1); + + click('.toggle-summary button'); + andThen(() => assert.ok(this.summaryToggled)); + } +}); + +widgetTest("pm map", { + template: '{{mount-widget widget="post" args=args}}', + setup() { + this.set('args', { + showTopicMap: true, + showPMMap: true, + allowedGroups: [], + allowedUsers: [ Ember.Object.create({ username: 'eviltrout' }) ] + }); + }, + test(assert) { + assert.equal(this.$('.private-message-map').length, 1); + assert.equal(this.$('.private-message-map .user').length, 1); + } +}); diff --git a/test/javascripts/widgets/poster-name-test.js.es6 b/test/javascripts/widgets/poster-name-test.js.es6 new file mode 100644 index 000000000..5e6782625 --- /dev/null +++ b/test/javascripts/widgets/poster-name-test.js.es6 @@ -0,0 +1,67 @@ +import { moduleForWidget, widgetTest } from 'helpers/widget-test'; + +moduleForWidget('poster-name'); + +widgetTest('basic rendering', { + template: '{{mount-widget widget="poster-name" args=args}}', + setup() { + this.set('args', { + username: 'eviltrout', + usernameUrl: '/users/eviltrout', + name: 'Robin Ward', + user_title: 'Trout Master' }); + }, + test(assert) { + assert.ok(this.$('.names').length); + assert.ok(this.$('span.username').length); + assert.ok(this.$('a[data-auto-route=true]').length); + assert.ok(this.$('a[data-user-card=eviltrout]').length); + assert.equal(this.$('.username a').text(), 'eviltrout'); + assert.equal(this.$('.full-name a').text(), 'Robin Ward'); + assert.equal(this.$('.user-title').text(), 'Trout Master'); + } +}); + +widgetTest('extra classes and glyphs', { + template: '{{mount-widget widget="poster-name" args=args}}', + setup() { + this.set('args', { + username: 'eviltrout', + usernameUrl: '/users/eviltrout', + staff: true, + admin: true, + moderator: true, + new_user: true, + primary_group_name: 'fish' + }); + }, + test(assert) { + assert.ok(this.$('span.staff').length); + assert.ok(this.$('span.admin').length); + assert.ok(this.$('span.moderator').length); + assert.ok(this.$('i.fa-shield').length); + assert.ok(this.$('span.new-user').length); + assert.ok(this.$('span.fish').length); + } +}); + +widgetTest('disable display name on posts', { + template: '{{mount-widget widget="poster-name" args=args}}', + setup() { + this.siteSettings.display_name_on_posts = false; + this.set('args', { username: 'eviltrout', name: 'Robin Ward' }); + }, + test(assert) { + assert.equal(this.$('.full-name').length, 0); + } +}); + +widgetTest("doesn't render a name if it's similar to the username", { + template: '{{mount-widget widget="poster-name" args=args}}', + setup() { + this.set('args', { username: 'eviltrout', name: 'evil-trout' }); + }, + test(assert) { + assert.equal(this.$('.full-name').length, 0); + } +}); diff --git a/vendor/assets/javascripts/virtual-dom-amd.js b/vendor/assets/javascripts/virtual-dom-amd.js new file mode 100644 index 000000000..75ee9dccf --- /dev/null +++ b/vendor/assets/javascripts/virtual-dom-amd.js @@ -0,0 +1,4 @@ +// Just a wrapper from the standalone browserified virtual-dom +define("virtual-dom", [], function() { + return virtualDom; +}); diff --git a/vendor/assets/javascripts/virtual-dom.js b/vendor/assets/javascripts/virtual-dom.js new file mode 100644 index 000000000..ef7710be9 --- /dev/null +++ b/vendor/assets/javascripts/virtual-dom.js @@ -0,0 +1,1668 @@ +!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.virtualDom=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o