Version bump
This commit is contained in:
commit
ad8f2cbed1
@ -7,6 +7,7 @@ app/assets/javascripts/vendor.js
|
||||
app/assets/javascripts/locales/i18n.js
|
||||
app/assets/javascripts/defer/html-sanitizer-bundle.js
|
||||
app/assets/javascripts/discourse/lib/Markdown.Editor.js
|
||||
app/assets/javascripts/ember-addons
|
||||
jsapp/lib/Markdown.Editor.js
|
||||
lib/javascripts/locale/
|
||||
lib/javascripts/messageformat.js
|
||||
|
||||
@ -90,10 +90,7 @@
|
||||
"no-undef": 2,
|
||||
"no-unused-vars": 2,
|
||||
"no-with": 2,
|
||||
"semi": [
|
||||
0,
|
||||
"never"
|
||||
],
|
||||
"semi": 2,
|
||||
"strict": 0,
|
||||
"valid-typeof": 2,
|
||||
"wrap-iife": [
|
||||
|
||||
134
CONTRIBUTING.md
134
CONTRIBUTING.md
@ -1,129 +1,27 @@
|
||||
# Contributing to Discourse
|
||||
|
||||
## Before You Start
|
||||
## Important note for Developers
|
||||
|
||||
Anyone wishing to contribute to the **[Discourse/Discourse](https://github.com/discourse/discourse)** project **MUST read & sign the [Electronic Discourse Forums Contribution License Agreement](http://www.discourse.org/cla)**. The Discourse team is legally prevented from accepting any pull requests from users who have not signed the CLA first.
|
||||
Anyone wishing to contribute to the [github.com/discourse/discourse](https://github.com/discourse/discourse) project **must read & sign our [Contributor License Agreement](http://www.discourse.org/cla)**. The Discourse team is legally prevented from accepting any pull requests from users who have not signed the CLA first.
|
||||
|
||||
## Reporting Bugs
|
||||
For more information on
|
||||
|
||||
1. Always update to the most recent master release; the bug may already be resolved.
|
||||
- how to set up your development environment
|
||||
- first-time project suggestions
|
||||
- code conventions
|
||||
- step-by-step guide for GitHub commits
|
||||
|
||||
2. Search for similar issues on the [Discourse meta forum][m]; it may already be an identified problem.
|
||||
**please read our [Discourse Development Contribution Guidelines](https://meta.discourse.org/t/discourse-development-contribution-guidelines/3823)**
|
||||
|
||||
3. Make sure you can reproduce your problem on our sandbox at [try.discourse.org](http://try.discourse.org)
|
||||
## Everything Else
|
||||
|
||||
4. If this is a bug or problem that **requires any kind of extended discussion -- open [a topic on meta][m] about it**.
|
||||
There are many other ways to contribute to Discourse besides code. We've outlined the most common ones below.
|
||||
|
||||
5. If possible, submit a Pull Request with a failing test. If you'd rather take matters into your own hands, fix the bug yourself (jump down to the "Contributing (Step-by-step)" section).
|
||||
- [Reporting Bugs](https://meta.discourse.org/t/how-to-make-bug-reports-for-discourse/33070)
|
||||
- [Requesting Features](https://meta.discourse.org/t/how-to-request-new-features-for-discourse/32986)
|
||||
- [Translation](https://meta.discourse.org/t/contribute-a-translation-to-discourse/14882)
|
||||
- Documentation (TBA)
|
||||
|
||||
6. When the bug is fixed, we will do our best to update the Discourse topic.
|
||||
For anything else, just start a new topic on [Meta](https://meta.discourse.org/) and let us know what you're interested in working on.
|
||||
|
||||
## Requesting New Features
|
||||
|
||||
1. Do not submit a feature request on GitHub; all feature requests on GitHub will be closed. Instead, visit the **[Discourse meta forum, features category](http://meta.discourse.org/category/feature)**, and search this list for similar feature requests. It's possible somebody has already asked for this feature or provided a pull request that we're still discussing.
|
||||
|
||||
2. Provide a clear and detailed explanation of the feature you want and why it's important to add. The feature must apply to a wide array of users of Discourse; for smaller, more targeted "one-off" features, you might consider writing a plugin for Discourse. You may also want to provide us with some advance documentation on the feature, which will help the community to better understand where it will fit.
|
||||
|
||||
3. If you're a Rock Star programmer, build the feature yourself (refer to the "Contributing (Step-by-step)" section below).
|
||||
|
||||
## Contributing (Step-by-step)
|
||||
|
||||
1. Clone the Repo:
|
||||
|
||||
git clone git://github.com/discourse/discourse.git
|
||||
|
||||
2. Create a new Branch:
|
||||
|
||||
cd discourse
|
||||
git checkout -b new_discourse_branch
|
||||
|
||||
> Please keep your code clean: one feature or bug-fix per branch. If you find another bug, you want to fix while being in a new branch, please fix it in a separated branch instead.
|
||||
|
||||
3. Code
|
||||
* Adhere to common conventions you see in the existing code
|
||||
* Include tests, and ensure they pass
|
||||
* Search to see if your new functionality has been discussed on [the Discourse meta forum](http://meta.discourse.org), and include updates as appropriate
|
||||
|
||||
4. Follow the Coding Conventions
|
||||
* two spaces, no tabs
|
||||
* no trailing whitespaces, blank lines should have no spaces
|
||||
* use spaces around operators, after commas, colons, semicolons, around `{` and before `}`
|
||||
* no space after `(`, `[` or before `]`, `)`
|
||||
* use Ruby 1.9 hash syntax: prefer `{ a: 1 }` over `{ :a => 1 }`
|
||||
* prefer `class << self; def method; end` over `def self.method` for class methods
|
||||
* prefer `{ ... }` over `do ... end` for single-line blocks, avoid using `{ ... }` for multi-line blocks
|
||||
* avoid `return` when not required
|
||||
|
||||
> However, please note that **pull requests consisting entirely of style changes are not welcome on this project**. Style changes in the context of pull requests that also refactor code, fix bugs, improve functionality *are* welcome.
|
||||
|
||||
5. Commit
|
||||
|
||||
For every commit please write a short (max 72 characters) summary in the first line followed with a blank line and then more detailed descriptions of the change. Use markdown syntax for simple styling.
|
||||
|
||||
**NEVER leave the commit message blank!** Provide a detailed, clear, and complete description of your commit!
|
||||
|
||||
|
||||
6. Update your branch
|
||||
|
||||
```
|
||||
git fetch origin
|
||||
git rebase origin/master
|
||||
```
|
||||
|
||||
7. Fork
|
||||
|
||||
```
|
||||
git remote add mine git@github.com:<your user name>/discourse.git
|
||||
```
|
||||
|
||||
8. Push to your remote
|
||||
|
||||
```
|
||||
git push mine new_discourse_branch
|
||||
```
|
||||
|
||||
9. Issue a Pull Request
|
||||
|
||||
Before submitting a pull-request, clean up the history, go over your commits and squash together minor changes and fixes into the corresponding commits. You can squash commits with the interactive rebase command:
|
||||
|
||||
```
|
||||
git fetch origin
|
||||
git checkout new_discourse_branch
|
||||
git rebase origin/master
|
||||
git rebase -i
|
||||
|
||||
< the editor opens and allows you to change the commit history >
|
||||
< follow the instructions on the bottom of the editor >
|
||||
|
||||
git push -f mine new_discourse_branch
|
||||
```
|
||||
|
||||
|
||||
In order to make a pull request,
|
||||
* Navigate to the Discourse repository you just pushed to (e.g. https://github.com/your-user-name/discourse)
|
||||
* Click "Pull Request".
|
||||
* Write your branch name in the branch field (this is filled with "master" by default)
|
||||
* Click "Update Commit Range".
|
||||
* Ensure the changesets you introduced are included in the "Commits" tab.
|
||||
* Ensure that the "Files Changed" incorporate all of your changes.
|
||||
* Fill in some details about your potential patch including a meaningful title.
|
||||
* Click "Send pull request".
|
||||
|
||||
Thanks for that -- we'll get to your pull request ASAP, we love pull requests!
|
||||
|
||||
10. Responding to Feedback
|
||||
|
||||
The Discourse team may recommend adjustments to your code. Part of interacting with a healthy open-source community requires you to be open to learning new techniques and strategies; *don't get discouraged!* Remember: if the Discourse team suggest changes to your code, **they care enough about your work that they want to include it**, and hope that you can assist by implementing those revisions on your own.
|
||||
|
||||
> Though we ask you to clean your history and squash commit before submitting a pull-request, please do not change any commits you've submitted already (as other work might be build on top).
|
||||
|
||||
## Translations
|
||||
|
||||
Translators can do their work in our [Transifex project](https://www.transifex.com/projects/p/discourse-org/). For more information, please see these how-to topics:
|
||||
|
||||
* [Contributing a translation to Discourse](https://meta.discourse.org/t/contribute-a-translation-to-discourse/14882)
|
||||
* [How to add a new language](https://meta.discourse.org/t/how-to-add-a-new-language/14970)
|
||||
|
||||
|
||||
|
||||
[m]: http://meta.discourse.org
|
||||
*Thanks for contributing!*
|
||||
|
||||
3
Gemfile
3
Gemfile
@ -63,7 +63,8 @@ gem 'email_reply_parser'
|
||||
|
||||
# note: for image_optim to correctly work you need to follow
|
||||
# https://github.com/toy/image_optim
|
||||
gem 'image_optim'
|
||||
# pinned due to https://github.com/toy/image_optim/pull/75, docker image must be upgraded to upgrade
|
||||
gem 'image_optim', '0.20.2'
|
||||
gem 'multi_json'
|
||||
gem 'mustache'
|
||||
gem 'nokogiri'
|
||||
|
||||
@ -209,7 +209,7 @@ GEM
|
||||
omniauth-twitter (1.0.1)
|
||||
multi_json (~> 1.3)
|
||||
omniauth-oauth (~> 1.0)
|
||||
onebox (1.5.24)
|
||||
onebox (1.5.25)
|
||||
moneta (~> 0.8)
|
||||
multi_json (~> 1.11)
|
||||
mustache
|
||||
@ -422,7 +422,7 @@ DEPENDENCIES
|
||||
highline
|
||||
hiredis
|
||||
htmlentities
|
||||
image_optim
|
||||
image_optim (= 0.20.2)
|
||||
librarian (>= 0.0.25)
|
||||
listen (= 0.7.3)
|
||||
logster
|
||||
@ -488,3 +488,6 @@ DEPENDENCIES
|
||||
uglifier
|
||||
unf
|
||||
unicorn
|
||||
|
||||
BUNDLED WITH
|
||||
1.10.6
|
||||
|
||||
@ -13,7 +13,7 @@ export default Ember.Controller.extend({
|
||||
}.property('problems'),
|
||||
|
||||
thereWereProblems: function() {
|
||||
if(!Discourse.User.currentProp('admin')) { return false }
|
||||
if(!Discourse.User.currentProp('admin')) { return false; }
|
||||
if( this.get('foundProblems') ) {
|
||||
this.set('hadProblems', true);
|
||||
return true;
|
||||
|
||||
@ -39,7 +39,7 @@ export default Ember.Controller.extend({
|
||||
if (this.get("showingLast")) { return; }
|
||||
|
||||
const group = this.get("model"),
|
||||
offset = Math.min(group.get("offset") + group.get("model.limit"), group.get("user_count"));
|
||||
offset = Math.min(group.get("offset") + group.get("limit"), group.get("user_count"));
|
||||
|
||||
group.set("offset", offset);
|
||||
|
||||
@ -50,7 +50,7 @@ export default Ember.Controller.extend({
|
||||
if (this.get("showingFirst")) { return; }
|
||||
|
||||
const group = this.get("model"),
|
||||
offset = Math.max(group.get("offset") - group.get("model.limit"), 0);
|
||||
offset = Math.max(group.get("offset") - group.get("limit"), 0);
|
||||
|
||||
group.set("offset", offset);
|
||||
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
import { exportEntity } from 'discourse/lib/export-csv';
|
||||
import { outputExportResult } from 'discourse/lib/export-result';
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
viewMode: 'table',
|
||||
viewingTable: Em.computed.equal('viewMode', 'table'),
|
||||
@ -30,6 +33,15 @@ export default Ember.Controller.extend({
|
||||
|
||||
viewAsBarChart() {
|
||||
this.set('viewMode', 'barChart');
|
||||
},
|
||||
|
||||
exportCsv() {
|
||||
exportEntity('report', {
|
||||
name: this.get("model.type"),
|
||||
start_date: this.get('startDate'),
|
||||
end_date: this.get('endDate'),
|
||||
category_id: this.get('categoryId') === 'all' ? undefined : this.get('categoryId')
|
||||
}).then(outputExportResult);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -56,8 +56,8 @@ export default Ember.Controller.extend({
|
||||
saveAll: function(){
|
||||
var self = this;
|
||||
var items = this.get('workingCopy');
|
||||
var groupIds = items.map(function(i){return i.get("id") || -1});
|
||||
var names = items.map(function(i){return i.get("name")});
|
||||
var groupIds = items.map(function(i){return i.get("id") || -1;});
|
||||
var names = items.map(function(i){return i.get("name");});
|
||||
|
||||
Discourse.ajax('/admin/badges/badge_groupings',{
|
||||
data: {ids: groupIds, names: names},
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
<label>
|
||||
{{input type="checkbox" checked=enabled}}
|
||||
{{{unbound setting.description}}}
|
||||
{{setting-validation-message message=validationMessage}}
|
||||
</label>
|
||||
|
||||
@ -13,9 +13,9 @@
|
||||
<div>
|
||||
<label>{{i18n 'admin.groups.group_members'}} ({{model.user_count}})</label>
|
||||
<div>
|
||||
<a {{bind-attr class=":previous showingFirst:disabled"}} {{action "previous"}}>{{fa-icon "fast-backward"}}</a>
|
||||
<a class="previous {{if showingFirst 'disabled'}}" {{action "previous"}}>{{fa-icon "fast-backward"}}</a>
|
||||
{{currentPage}}/{{totalPages}}
|
||||
<a {{bind-attr class=":next showingLast:disabled"}} {{action "next"}}>{{fa-icon "fast-forward"}}</a>
|
||||
<a class="next {{if showingLast 'disabled'}}" {{action "next"}}>{{fa-icon "fast-forward"}}</a>
|
||||
</div>
|
||||
<div class="ac-wrap clearfix">
|
||||
{{#each model.members as |member|}}
|
||||
@ -28,7 +28,7 @@
|
||||
<div>
|
||||
<label for="user-selector">{{i18n 'admin.groups.add_members'}}</label>
|
||||
{{user-selector usernames=model.usernames placeholderKey="admin.groups.selector_placeholder" id="user-selector"}}
|
||||
<button {{action "addMembers"}} class='btn add'>{{fa-icon "plus"}} {{i18n 'admin.groups.add'}}</button>
|
||||
{{d-button action="addMembers" class="add" icon="plus" label="admin.groups.add"}}
|
||||
</div>
|
||||
{{/unless}}
|
||||
{{/if}}
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
{{i18n 'admin.dashboard.reports.end_date'}} {{input type="date" value=endDate}}
|
||||
{{combo-box valueAttribute="value" content=categoryOptions value=categoryId}}
|
||||
{{d-button action="refreshReport" class="btn-primary" label="admin.dashboard.reports.refresh_report" icon="refresh"}}
|
||||
{{d-button action="exportCsv" label="admin.export_csv.button_text" icon="download"}}
|
||||
</div>
|
||||
|
||||
<div class='view-options'>
|
||||
|
||||
@ -152,10 +152,10 @@ function proxyDep(propName, moduleFunc, msg) {
|
||||
});
|
||||
}
|
||||
|
||||
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");
|
||||
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");
|
||||
|
||||
@ -2,8 +2,8 @@ import computed from "ember-addons/ember-computed-decorators";
|
||||
import { observes } from "ember-addons/ember-computed-decorators";
|
||||
|
||||
export default Ember.Component.extend({
|
||||
autoCloseValid: false,
|
||||
limited: false,
|
||||
autoCloseValid: false,
|
||||
|
||||
@computed("limited")
|
||||
autoCloseUnits(limited) {
|
||||
@ -19,15 +19,14 @@ export default Ember.Component.extend({
|
||||
|
||||
@observes("autoCloseTime", "limited")
|
||||
_updateAutoCloseValid() {
|
||||
const autoCloseTime = this.get("autoCloseTime");
|
||||
const limited = this.get("limited");
|
||||
|
||||
var isValid = this._isAutoCloseValid(autoCloseTime, limited);
|
||||
const limited = this.get("limited"),
|
||||
autoCloseTime = this.get("autoCloseTime"),
|
||||
isValid = this._isAutoCloseValid(autoCloseTime, limited);
|
||||
this.set("autoCloseValid", isValid);
|
||||
},
|
||||
|
||||
_isAutoCloseValid(autoCloseTime, limited) {
|
||||
var t = (autoCloseTime || "").toString().trim();
|
||||
const t = (autoCloseTime || "").toString().trim();
|
||||
if (t.length === 0) {
|
||||
// "empty" is always valid
|
||||
return true;
|
||||
|
||||
@ -1,7 +1,12 @@
|
||||
import { on } from "ember-addons/ember-computed-decorators";
|
||||
|
||||
export default Ember.TextField.extend({
|
||||
becomeFocused: function() {
|
||||
var input = this.get("element");
|
||||
|
||||
@on("didInsertElement")
|
||||
becomeFocused() {
|
||||
const input = this.get("element");
|
||||
input.focus();
|
||||
input.selectionStart = input.selectionEnd = input.value.length;
|
||||
}.on('didInsertElement')
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
import UploadMixin from "discourse/mixins/upload";
|
||||
|
||||
export default Em.Component.extend(UploadMixin, {
|
||||
@ -5,21 +6,23 @@ export default Em.Component.extend(UploadMixin, {
|
||||
tagName: "span",
|
||||
imageIsNotASquare: false,
|
||||
|
||||
uploadButtonText: function() {
|
||||
return this.get("uploading") ? I18n.t("uploading") : I18n.t("user.change_avatar.upload_picture");
|
||||
}.property("uploading"),
|
||||
@computed("uploading")
|
||||
uploadButtonText(uploading) {
|
||||
return uploading ? I18n.t("uploading") : I18n.t("user.change_avatar.upload_picture");
|
||||
},
|
||||
|
||||
uploadDone(upload) {
|
||||
this.setProperties({
|
||||
imageIsNotASquare: upload.width !== upload.height,
|
||||
uploadedAvatarTemplate: upload.url,
|
||||
custom_avatar_upload_id: upload.id,
|
||||
uploadedAvatarId: upload.id,
|
||||
});
|
||||
|
||||
this.sendAction("done");
|
||||
},
|
||||
|
||||
data: function() {
|
||||
return { user_id: this.get("user_id") };
|
||||
}.property("user_id")
|
||||
@computed("user_id")
|
||||
data(user_id) {
|
||||
return { user_id };
|
||||
}
|
||||
});
|
||||
|
||||
@ -0,0 +1,39 @@
|
||||
import { iconHTML } from 'discourse/helpers/fa-icon';
|
||||
import DropdownButton from 'discourse/components/dropdown-button';
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
|
||||
export default DropdownButton.extend({
|
||||
buttonExtraClasses: 'no-text',
|
||||
title: '',
|
||||
text: iconHTML('bars') + ' ' + iconHTML('caret-down'),
|
||||
classNames: ['category-notification-menu', 'category-admin-menu'],
|
||||
|
||||
@computed()
|
||||
dropDownContent() {
|
||||
const includeReorder = this.get('siteSettings.fixed_category_positions');
|
||||
const items = [
|
||||
{ id: 'create',
|
||||
title: I18n.t('category.create'),
|
||||
description: I18n.t('category.create_long'),
|
||||
styleClasses: 'fa fa-plus' }
|
||||
];
|
||||
if (includeReorder) {
|
||||
items.push({
|
||||
id: 'reorder',
|
||||
title: I18n.t('categories.reorder.title'),
|
||||
description: I18n.t('categories.reorder.title_long'),
|
||||
styleClasses: 'fa fa-random'
|
||||
});
|
||||
}
|
||||
return items;
|
||||
},
|
||||
|
||||
actionNames: {
|
||||
create: 'createCategory',
|
||||
reorder: 'reorderCategories'
|
||||
},
|
||||
|
||||
clicked(id) {
|
||||
this.sendAction('actionNames.' + id);
|
||||
}
|
||||
});
|
||||
@ -47,7 +47,7 @@ export default Ember.Component.extend({
|
||||
|
||||
if (color) {
|
||||
var style = "";
|
||||
if (color) { style += "background-color: #" + color + ";" }
|
||||
if (color) { style += "background-color: #" + color + ";"; }
|
||||
return style.htmlSafe();
|
||||
}
|
||||
}
|
||||
|
||||
@ -29,9 +29,7 @@ export default Ember.Component.extend(StringBuffer, {
|
||||
buffer.push("<h4 class='title'>" + title + "</h4>");
|
||||
}
|
||||
|
||||
buffer.push("<button class='btn standard dropdown-toggle' data-toggle='dropdown'>");
|
||||
buffer.push(this.get('text'));
|
||||
buffer.push("</button>");
|
||||
buffer.push(`<button class='btn standard dropdown-toggle ${this.get('buttonExtraClasses')}' data-toggle='dropdown'>${this.get('text')}</button>`);
|
||||
buffer.push("<ul class='dropdown-menu'>");
|
||||
|
||||
const contents = this.get('dropDownContent');
|
||||
|
||||
@ -7,16 +7,24 @@ export default buildCategoryPanel('security', {
|
||||
|
||||
actions: {
|
||||
editPermissions() {
|
||||
this.set('editingPermissions', true);
|
||||
if (!this.get('category.is_special')) {
|
||||
this.set('editingPermissions', true);
|
||||
}
|
||||
},
|
||||
|
||||
addPermission(group, id) {
|
||||
this.get('category').addPermission({group_name: group + "",
|
||||
permission: Discourse.PermissionType.create({id})});
|
||||
if (!this.get('category.is_special')) {
|
||||
this.get('category').addPermission({
|
||||
group_name: group + "",
|
||||
permission: Discourse.PermissionType.create({id})
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
removePermission(permission) {
|
||||
this.get('category').removePermission(permission);
|
||||
if (!this.get('category.is_special')) {
|
||||
this.get('category').removePermission(permission);
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
@ -25,7 +25,7 @@ export default Ember.Component.extend({
|
||||
}
|
||||
|
||||
return groups.filter(function(group){
|
||||
return !selectedGroups.any(function(s){return s === group.name});
|
||||
return !selectedGroups.any(function(s){return s === group.name;});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
@ -2,6 +2,12 @@ import computed from 'ember-addons/ember-computed-decorators';
|
||||
export default Ember.Component.extend({
|
||||
classNames: ['hamburger-panel'],
|
||||
|
||||
@computed('currentUser.read_faq')
|
||||
prioritizeFaq(readFaq) {
|
||||
// If it's a custom FAQ never prioritize it
|
||||
return Ember.isEmpty(this.siteSettings.faq_url) && !readFaq;
|
||||
},
|
||||
|
||||
@computed()
|
||||
showKeyboardShortcuts() {
|
||||
return !Discourse.Mobile.mobileView && !this.capabilities.touch;
|
||||
|
||||
@ -28,11 +28,9 @@ export default Ember.Component.extend({
|
||||
const $buttonPanel = $('header ul.icons');
|
||||
if ($buttonPanel.length === 0) { return; }
|
||||
|
||||
const buttonPanelPos = $buttonPanel.offset();
|
||||
const posTop = parseInt(buttonPanelPos.top + $buttonPanel.height() - $('header.d-header').offset().top);
|
||||
const posLeft = parseInt(buttonPanelPos.left + $buttonPanel.width() - width);
|
||||
|
||||
this.$().css({ left: posLeft + "px", top: posTop + "px", height: 'auto' });
|
||||
// These values need to be set here, not in the css file - this is to deal with the
|
||||
// possibility of the window being resized and the menu changing from .slide-in to .drop-down.
|
||||
this.$().css({ top: '100%', height: 'auto' });
|
||||
|
||||
// adjust panel height
|
||||
const fullHeight = parseInt($window.height());
|
||||
@ -56,7 +54,7 @@ export default Ember.Component.extend({
|
||||
}
|
||||
|
||||
$panelBody.height('100%');
|
||||
this.$().css({ left: "auto", top: (menuTop - 2) + "px", height });
|
||||
this.$().css({ top: menuTop + "px", height });
|
||||
$('body').removeClass('drop-down-visible');
|
||||
}
|
||||
|
||||
|
||||
@ -1,24 +1,28 @@
|
||||
/* You might be looking for navigation-item. */
|
||||
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
|
||||
export default Ember.Component.extend({
|
||||
tagName: 'li',
|
||||
classNameBindings: ['active'],
|
||||
|
||||
router: function() {
|
||||
@computed()
|
||||
router() {
|
||||
return this.container.lookup('router:main');
|
||||
}.property(),
|
||||
},
|
||||
|
||||
fullPath: function() {
|
||||
return Discourse.getURL(this.get('path'));
|
||||
}.property('path'),
|
||||
@computed("path")
|
||||
fullPath(path) {
|
||||
return Discourse.getURL(path);
|
||||
},
|
||||
|
||||
active: function() {
|
||||
const route = this.get('route');
|
||||
@computed("route", "router.url")
|
||||
active(route) {
|
||||
if (!route) { return; }
|
||||
|
||||
const routeParam = this.get('routeParam'),
|
||||
router = this.get('router');
|
||||
|
||||
return routeParam ? router.isActive(route, routeParam) : router.isActive(route);
|
||||
}.property('router.url', 'route')
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,27 +1,25 @@
|
||||
import { default as computed, observes } from "ember-addons/ember-computed-decorators";
|
||||
import DiscourseURL from 'discourse/lib/url';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
tagName: 'ul',
|
||||
classNameBindings: [':nav', ':nav-pills'],
|
||||
id: 'navigation-bar',
|
||||
selectedNavItem: function(){
|
||||
const filterMode = this.get('filterMode'),
|
||||
navItems = this.get('navItems');
|
||||
|
||||
var item = navItems.find(function(i){
|
||||
return i.get('filterMode').indexOf(filterMode) === 0;
|
||||
});
|
||||
|
||||
@computed("filterMode", "navItems")
|
||||
selectedNavItem(filterMode, navItems){
|
||||
var item = navItems.find(i => i.get('filterMode').indexOf(filterMode) === 0);
|
||||
return item || navItems[0];
|
||||
}.property('filterMode'),
|
||||
},
|
||||
|
||||
closedNav: function(){
|
||||
@observes("expanded")
|
||||
closedNav() {
|
||||
if (!this.get('expanded')) {
|
||||
this.ensureDropClosed();
|
||||
}
|
||||
}.observes('expanded'),
|
||||
},
|
||||
|
||||
ensureDropClosed: function(){
|
||||
ensureDropClosed() {
|
||||
if (!this.get('expanded')) {
|
||||
this.set('expanded',false);
|
||||
}
|
||||
@ -30,25 +28,23 @@ export default Ember.Component.extend({
|
||||
},
|
||||
|
||||
actions: {
|
||||
toggleDrop: function(){
|
||||
toggleDrop() {
|
||||
this.set('expanded', !this.get('expanded'));
|
||||
var self = this;
|
||||
if (this.get('expanded')) {
|
||||
|
||||
if (this.get('expanded')) {
|
||||
DiscourseURL.appEvents.on('dom:clean', this, this.ensureDropClosed);
|
||||
|
||||
Em.run.next(function() {
|
||||
Em.run.next(() => {
|
||||
if (!this.get('expanded')) { return; }
|
||||
|
||||
if (!self.get('expanded')) { return; }
|
||||
|
||||
self.$('.drop a').on('click', function(){
|
||||
self.$('.drop').hide();
|
||||
self.set('expanded', false);
|
||||
this.$('.drop a').on('click', () => {
|
||||
this.$('.drop').hide();
|
||||
this.set('expanded', false);
|
||||
return true;
|
||||
});
|
||||
|
||||
$(window).on('click.navigation-bar', function() {
|
||||
self.set('expanded', false);
|
||||
$(window).on('click.navigation-bar', () => {
|
||||
this.set('expanded', false);
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
import StringBuffer from 'discourse/mixins/string-buffer';
|
||||
|
||||
export default Ember.Component.extend(StringBuffer, {
|
||||
@ -7,22 +8,23 @@ export default Ember.Component.extend(StringBuffer, {
|
||||
hidden: Em.computed.not('content.visible'),
|
||||
rerenderTriggers: ['content.count'],
|
||||
|
||||
title: function() {
|
||||
var categoryName = this.get('content.categoryName'),
|
||||
name = this.get('content.name'),
|
||||
extra = {};
|
||||
@computed("content.categoryName", "content.name")
|
||||
title(categoryName, name) {
|
||||
const extra = {};
|
||||
|
||||
if (categoryName) {
|
||||
name = "category";
|
||||
extra.categoryName = categoryName;
|
||||
}
|
||||
return I18n.t("filters." + name.replace("/", ".") + ".help", extra);
|
||||
}.property("content.{categoryName,name}"),
|
||||
|
||||
active: function() {
|
||||
return this.get('content.filterMode') === this.get('filterMode') ||
|
||||
this.get('filterMode').indexOf(this.get('content.filterMode')) === 0;
|
||||
}.property('content.filterMode', 'filterMode'),
|
||||
return I18n.t("filters." + name.replace("/", ".") + ".help", extra);
|
||||
},
|
||||
|
||||
@computed("content.filterMode", "filterMode")
|
||||
active(contentFilterMode, filterMode) {
|
||||
return contentFilterMode === filterMode ||
|
||||
filterMode.indexOf(contentFilterMode) === 0;
|
||||
},
|
||||
|
||||
renderString(buffer) {
|
||||
const content = this.get('content');
|
||||
|
||||
@ -0,0 +1,45 @@
|
||||
import { on } from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
classNameBindings: ["visible::hidden", ":popup-menu"],
|
||||
|
||||
@on('didInsertElement')
|
||||
_setup() {
|
||||
this.appEvents.on("popup-menu:open", this, "_changeLocation");
|
||||
|
||||
$('html').on(`mouseup.popup-menu-${this.get('elementId')}`, (e) => {
|
||||
const $target = $(e.target);
|
||||
if ($target.is("button") || this.$().has($target).length === 0) {
|
||||
this.sendAction('hide');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@on('willDestroyElement')
|
||||
_cleanup() {
|
||||
$('html').off(`mouseup.popup-menu-${this.get('elementId')}`);
|
||||
this.appEvents.off("popup-menu:open", this, "_changeLocation");
|
||||
},
|
||||
|
||||
_changeLocation(location) {
|
||||
const $this = this.$();
|
||||
switch (location.position) {
|
||||
case "absolute": {
|
||||
$this.css({
|
||||
position: "absolute",
|
||||
top: location.top - $this.innerHeight() + 5,
|
||||
left: location.left,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "fixed": {
|
||||
$this.css({
|
||||
position: "fixed",
|
||||
top: location.top,
|
||||
left: location.left - $this.innerWidth(),
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -369,13 +369,13 @@ const PostMenuComponent = Ember.Component.extend(StringBuffer, {
|
||||
unhidePostIcon = iconHTML('eye'),
|
||||
unhidePostText = I18n.t('post.controls.unhide');
|
||||
|
||||
const html = '<div class="post-admin-menu">' +
|
||||
const html = '<div class="post-admin-menu popup-menu">' +
|
||||
'<h3>' + I18n.t('admin_title') + '</h3>' +
|
||||
'<ul>' +
|
||||
'<li class="btn btn-admin" data-action="toggleWiki">' + wikiIcon + wikiText + '</li>' +
|
||||
(Discourse.User.currentProp('staff') ? '<li class="btn btn-admin" data-action="togglePostType">' + postTypeIcon + postTypeText + '</li>' : '') +
|
||||
'<li class="btn btn-admin" data-action="rebakePost">' + rebakePostIcon + rebakePostText + '</li>' +
|
||||
(post.hidden ? '<li class="btn btn-admin" data-action="unhidePost">' + unhidePostIcon + unhidePostText + '</li>' : '') +
|
||||
'<li class="btn" data-action="toggleWiki">' + wikiIcon + wikiText + '</li>' +
|
||||
(Discourse.User.currentProp('staff') ? '<li class="btn" data-action="togglePostType">' + postTypeIcon + postTypeText + '</li>' : '') +
|
||||
'<li class="btn" data-action="rebakePost">' + rebakePostIcon + rebakePostText + '</li>' +
|
||||
(post.hidden ? '<li class="btn" data-action="unhidePost">' + unhidePostIcon + unhidePostText + '</li>' : '') +
|
||||
'</ul>' +
|
||||
'</div>';
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import {searchForTerm, searchContextDescription} from 'discourse/lib/search';
|
||||
import {searchForTerm, searchContextDescription, isValidSearchTerm } from 'discourse/lib/search';
|
||||
import DiscourseURL from 'discourse/lib/url';
|
||||
import { default as computed, observes } from 'ember-addons/ember-computed-decorators';
|
||||
import showModal from 'discourse/lib/show-modal';
|
||||
@ -61,8 +61,8 @@ export default Ember.Component.extend({
|
||||
@observes('searchService.term', 'typeFilter')
|
||||
newSearchNeeded() {
|
||||
this.set('noResults', false);
|
||||
const term = (this.get('searchService.term') || '').trim();
|
||||
if (term.length >= Discourse.SiteSettings.min_search_term_length) {
|
||||
const term = this.get('searchService.term');
|
||||
if (isValidSearchTerm(term)) {
|
||||
this.set('loading', true);
|
||||
Ember.run.debounce(this, 'searchTerm', term, this.get('typeFilter'), 400);
|
||||
} else {
|
||||
@ -134,7 +134,7 @@ export default Ember.Component.extend({
|
||||
},
|
||||
|
||||
showedSearch() {
|
||||
$('#search-term').focus();
|
||||
$('#search-term').focus().select();
|
||||
},
|
||||
|
||||
showSearchHelp() {
|
||||
@ -154,8 +154,7 @@ export default Ember.Component.extend({
|
||||
},
|
||||
|
||||
keyDown(e) {
|
||||
const term = this.get('searchService.term');
|
||||
if (e.which === 13 && term && term.length >= this.siteSettings.min_search_term_length) {
|
||||
if (e.which === 13 && isValidSearchTerm(this.get('searchService.term'))) {
|
||||
this.set('visible', false);
|
||||
this.send('fullSearch');
|
||||
}
|
||||
|
||||
@ -1,9 +1,19 @@
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
import { on } from 'ember-addons/ember-computed-decorators';
|
||||
import TextField from 'discourse/components/text-field';
|
||||
|
||||
export default TextField.extend({
|
||||
@computed('searchService.searchContextEnabled')
|
||||
placeholder: function(searchContextEnabled) {
|
||||
placeholder(searchContextEnabled) {
|
||||
return searchContextEnabled ? "" : I18n.t('search.title');
|
||||
},
|
||||
|
||||
@on("didInsertElement")
|
||||
becomeFocused() {
|
||||
if (!this.get('hasAutofocus')) { return; }
|
||||
// iOS is crazy, without this we will not be
|
||||
// at the top of the page
|
||||
$(window).scrollTop(0);
|
||||
this.$().focus();
|
||||
}
|
||||
});
|
||||
|
||||
@ -0,0 +1,22 @@
|
||||
import DButton from 'discourse/components/d-button';
|
||||
|
||||
export default DButton.extend({
|
||||
click() {
|
||||
const $target = this.$(),
|
||||
position = $target.position(),
|
||||
width = $target.innerWidth(),
|
||||
loc = {
|
||||
position: this.get('position') || "fixed",
|
||||
left: position.left + width,
|
||||
top: position.top
|
||||
};
|
||||
|
||||
// TODO views/topic-footer-buttons is instantiating this via attachViewWithArgs
|
||||
// attachViewWithArgs does not set this.appEvents, it is undefined
|
||||
// this is a workaround but a proper fix probably depends on either deprecation
|
||||
// of attachViewClass et.el or correction of the methods to hydrate the depndencies
|
||||
this.appEvents = this.appEvents || this.container.lookup('app-events:main');
|
||||
this.appEvents.trigger("popup-menu:open", loc);
|
||||
this.sendAction("action");
|
||||
}
|
||||
});
|
||||
@ -1,24 +0,0 @@
|
||||
export default Em.Component.extend({
|
||||
tagName: "button",
|
||||
classNames: ["btn", "no-text", "show-topic-admin"],
|
||||
attributeBindings: ["title"],
|
||||
title: I18n.t("topic_admin_menu"),
|
||||
|
||||
render: function(buffer) {
|
||||
buffer.push("<i class='fa fa-wrench'></i>");
|
||||
},
|
||||
|
||||
click: function() {
|
||||
var $target = this.$(),
|
||||
position = $target.position(),
|
||||
width = $target.innerWidth();
|
||||
var location = {
|
||||
position: "fixed",
|
||||
left: position.left + width,
|
||||
top: position.top,
|
||||
};
|
||||
this.appEvents.trigger("topic-admin-menu:open", location);
|
||||
this.sendAction("show");
|
||||
return false;
|
||||
}
|
||||
});
|
||||
@ -1,19 +1,10 @@
|
||||
/**
|
||||
This is a custom text field that allows i18n placeholders
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
|
||||
@class TextField
|
||||
@extends Ember.TextField
|
||||
@namespace Discourse
|
||||
@module Discourse
|
||||
**/
|
||||
export default Ember.TextField.extend({
|
||||
attributeBindings: ['autocorrect', 'autocapitalize', 'autofocus', 'maxLength'],
|
||||
|
||||
placeholder: function() {
|
||||
if (this.get('placeholderKey')) {
|
||||
return I18n.t(this.get('placeholderKey'));
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}.property('placeholderKey')
|
||||
@computed("placeholderKey")
|
||||
placeholder(placeholderKey) {
|
||||
return placeholderKey ? I18n.t(placeholderKey) : "";
|
||||
}
|
||||
});
|
||||
|
||||
@ -2,6 +2,8 @@ import SmallActionComponent from 'discourse/components/small-action';
|
||||
|
||||
export default SmallActionComponent.extend({
|
||||
classNames: ['time-gap'],
|
||||
classNameBindings: ['hideTimeGap::hidden'],
|
||||
hideTimeGap: Em.computed.alias('postStream.hasNoFilters'),
|
||||
icon: 'clock-o',
|
||||
|
||||
description: function() {
|
||||
|
||||
@ -11,4 +11,4 @@ export default Ember.Component.extend({
|
||||
list.removeObject(id);
|
||||
}
|
||||
}.observes('selected')
|
||||
})
|
||||
});
|
||||
|
||||
@ -13,7 +13,7 @@ export default Ember.Component.extend(StringBuffer, {
|
||||
iconsHtml += "<a href=\"" + Discourse.getURL("/users/") + u.get('username_lower') + "\" data-user-card=\"" + u.get('username_lower') + "\">";
|
||||
iconsHtml += Discourse.Utilities.avatarImg({
|
||||
size: 'small',
|
||||
avatarTemplate: u.get('avatarTemplate'),
|
||||
avatarTemplate: u.get('avatar_template'),
|
||||
title: u.get('username')
|
||||
});
|
||||
iconsHtml += "</a>";
|
||||
|
||||
@ -1,21 +1,29 @@
|
||||
import ModalFunctionality from 'discourse/mixins/modal-functionality';
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
||||
|
||||
export default Ember.Controller.extend(ModalFunctionality, {
|
||||
uploadedAvatarTemplate: null,
|
||||
saveDisabled: Em.computed.alias("uploading"),
|
||||
hasUploadedAvatar: Em.computed.or('uploadedAvatarTemplate', 'custom_avatar_upload_id'),
|
||||
|
||||
selectedUploadId: function() {
|
||||
switch (this.get("selected")) {
|
||||
case "system": return this.get("system_avatar_upload_id");
|
||||
case "gravatar": return this.get("gravatar_avatar_upload_id");
|
||||
default: return this.get("custom_avatar_upload_id");
|
||||
@computed("selected", "system_avatar_upload_id", "gravatar_avatar_upload_id", "custom_avatar_upload_id")
|
||||
selectedUploadId(selected, system, gravatar, custom) {
|
||||
switch (selected) {
|
||||
case "system": return system;
|
||||
case "gravatar": return gravatar;
|
||||
default: return custom;
|
||||
}
|
||||
}.property('selected', 'system_avatar_upload_id', 'gravatar_avatar_upload_id', 'custom_avatar_upload_id'),
|
||||
},
|
||||
|
||||
allowImageUpload: function() {
|
||||
@computed("selected", "system_avatar_template", "gravatar_avatar_template", "custom_avatar_template")
|
||||
selectedAvatarTemplate(selected, system, gravatar, custom) {
|
||||
switch (selected) {
|
||||
case "system": return system;
|
||||
case "gravatar": return gravatar;
|
||||
default: return custom;
|
||||
}
|
||||
},
|
||||
|
||||
@computed()
|
||||
allowImageUpload() {
|
||||
return Discourse.Utilities.allowsImages();
|
||||
}.property(),
|
||||
},
|
||||
|
||||
actions: {
|
||||
useUploadedAvatar() { this.set("selected", "uploaded"); },
|
||||
@ -25,8 +33,11 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
refreshGravatar() {
|
||||
this.set("gravatarRefreshDisabled", true);
|
||||
return Discourse
|
||||
.ajax("/user_avatar/" + this.get("username") + "/refresh_gravatar.json", { method: 'POST' })
|
||||
.then(result => this.set("gravatar_avatar_upload_id", result.upload_id))
|
||||
.ajax(`/user_avatar/${this.get("username")}/refresh_gravatar.json`, { method: "POST" })
|
||||
.then(result => this.setProperties({
|
||||
gravatar_avatar_template: result.gravatar_avatar_template,
|
||||
gravatar_upload_id: result.gravatar_upload_id,
|
||||
}))
|
||||
.finally(() => this.set("gravatarRefreshDisabled", false));
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ import DiscourseURL from 'discourse/lib/url';
|
||||
import Quote from 'discourse/lib/quote';
|
||||
import Draft from 'discourse/models/draft';
|
||||
import Composer from 'discourse/models/composer';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
function loadDraft(store, opts) {
|
||||
opts = opts || {};
|
||||
@ -54,6 +55,7 @@ export default Ember.Controller.extend({
|
||||
similarTopics: null,
|
||||
similarTopicsMessage: null,
|
||||
lastSimilaritySearch: null,
|
||||
optionsVisible: false,
|
||||
|
||||
topic: null,
|
||||
|
||||
@ -64,6 +66,12 @@ export default Ember.Controller.extend({
|
||||
this.set('similarTopics', []);
|
||||
}.on('init'),
|
||||
|
||||
@computed('model.action')
|
||||
canWhisper(action) {
|
||||
const currentUser = this.currentUser;
|
||||
return currentUser && currentUser.get('staff') && this.siteSettings.enable_whispers && action === Composer.REPLY;
|
||||
},
|
||||
|
||||
showWarning: function() {
|
||||
if (!Discourse.User.currentProp('staff')) { return false; }
|
||||
|
||||
@ -77,6 +85,20 @@ export default Ember.Controller.extend({
|
||||
}.property('model.creatingPrivateMessage', 'model.targetUsernames'),
|
||||
|
||||
actions: {
|
||||
|
||||
toggleWhisper() {
|
||||
this.toggleProperty('model.whisper');
|
||||
},
|
||||
|
||||
showOptions(loc) {
|
||||
this.appEvents.trigger('popup-menu:open', loc);
|
||||
this.set('optionsVisible', true);
|
||||
},
|
||||
|
||||
hideOptions() {
|
||||
this.set('optionsVisible', false);
|
||||
},
|
||||
|
||||
// Toggle the reply view
|
||||
toggle() {
|
||||
this.toggle();
|
||||
@ -132,7 +154,6 @@ export default Ember.Controller.extend({
|
||||
},
|
||||
|
||||
hitEsc() {
|
||||
|
||||
const messages = this.get('controllers.composer-messages.model');
|
||||
if (messages.length) {
|
||||
messages.popObject();
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { observes } from "ember-addons/ember-computed-decorators";
|
||||
import ModalFunctionality from 'discourse/mixins/modal-functionality';
|
||||
|
||||
// Modal related to auto closing of topics
|
||||
@ -5,31 +6,32 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
auto_close_valid: true,
|
||||
auto_close_invalid: Em.computed.not('auto_close_valid'),
|
||||
|
||||
setAutoCloseTime: function() {
|
||||
var autoCloseTime = null;
|
||||
@observes("model.details.auto_close_at", "model.details.auto_close_hours")
|
||||
setAutoCloseTime() {
|
||||
let autoCloseTime = null;
|
||||
|
||||
if (this.get("model.details.auto_close_based_on_last_post")) {
|
||||
autoCloseTime = this.get("model.details.auto_close_hours");
|
||||
} else if (this.get("model.details.auto_close_at")) {
|
||||
var closeTime = new Date(this.get("model.details.auto_close_at"));
|
||||
const closeTime = new Date(this.get("model.details.auto_close_at"));
|
||||
if (closeTime > new Date()) {
|
||||
autoCloseTime = moment(closeTime).format("YYYY-MM-DD HH:mm");
|
||||
}
|
||||
}
|
||||
|
||||
this.set("model.auto_close_time", autoCloseTime);
|
||||
}.observes("model.details.{auto_close_at,auto_close_hours}"),
|
||||
|
||||
actions: {
|
||||
saveAutoClose: function() { this.setAutoClose(this.get("model.auto_close_time")); },
|
||||
removeAutoClose: function() { this.setAutoClose(null); }
|
||||
},
|
||||
|
||||
setAutoClose: function(time) {
|
||||
var self = this;
|
||||
actions: {
|
||||
saveAutoClose() { this.setAutoClose(this.get("model.auto_close_time")); },
|
||||
removeAutoClose() { this.setAutoClose(null); }
|
||||
},
|
||||
|
||||
setAutoClose(time) {
|
||||
const self = this;
|
||||
this.send('hideModal');
|
||||
Discourse.ajax({
|
||||
url: '/t/' + this.get('model.id') + '/autoclose',
|
||||
url: `/t/${this.get('model.id')}/autoclose`,
|
||||
type: 'PUT',
|
||||
dataType: 'json',
|
||||
data: {
|
||||
@ -37,15 +39,15 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
auto_close_based_on_last_post: this.get("model.details.auto_close_based_on_last_post"),
|
||||
timezone_offset: (new Date().getTimezoneOffset())
|
||||
}
|
||||
}).then(function(result){
|
||||
}).then(result => {
|
||||
if (result.success) {
|
||||
self.send('closeModal');
|
||||
self.set('model.details.auto_close_at', result.auto_close_at);
|
||||
self.set('model.details.auto_close_hours', result.auto_close_hours);
|
||||
this.send('closeModal');
|
||||
this.set('model.details.auto_close_at', result.auto_close_at);
|
||||
this.set('model.details.auto_close_hours', result.auto_close_hours);
|
||||
} else {
|
||||
bootbox.alert(I18n.t('composer.auto_close.error'), function() { self.send('reopenModal'); } );
|
||||
}
|
||||
}, function () {
|
||||
}).catch(() => {
|
||||
bootbox.alert(I18n.t('composer.auto_close.error'), function() { self.send('reopenModal'); } );
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { translateResults, searchContextDescription, getSearchKey } from "discourse/lib/search";
|
||||
import { translateResults, searchContextDescription, getSearchKey, isValidSearchTerm } from "discourse/lib/search";
|
||||
import showModal from 'discourse/lib/show-modal';
|
||||
import { default as computed, observes } from 'ember-addons/ember-computed-decorators';
|
||||
import Category from 'discourse/models/category';
|
||||
@ -13,13 +13,18 @@ export default Ember.Controller.extend({
|
||||
context_id: null,
|
||||
context: null,
|
||||
|
||||
@computed('q')
|
||||
hasAutofocus(q) {
|
||||
return Em.isEmpty(q);
|
||||
},
|
||||
|
||||
@computed('skip_context', 'context')
|
||||
searchContextEnabled: {
|
||||
get(skip,context){
|
||||
return (!skip && context) || skip === "false";
|
||||
},
|
||||
set(val) {
|
||||
this.set('skip_context', val ? "false" : "true" )
|
||||
this.set('skip_context', val ? "false" : "true" );
|
||||
}
|
||||
},
|
||||
|
||||
@ -37,7 +42,12 @@ export default Ember.Controller.extend({
|
||||
|
||||
@computed('q')
|
||||
searchActive(q){
|
||||
return q && q.length > 0;
|
||||
return isValidSearchTerm(q);
|
||||
},
|
||||
|
||||
@computed('searchTerm')
|
||||
isNotValidSearchTerm(searchTerm) {
|
||||
return !isValidSearchTerm(searchTerm);
|
||||
},
|
||||
|
||||
@observes('model')
|
||||
@ -90,7 +100,7 @@ export default Ember.Controller.extend({
|
||||
const model = translateResults(results) || {};
|
||||
router.transientCache('lastSearch', { searchKey, model }, 5);
|
||||
this.set("model", model);
|
||||
}).finally(() => {this._searching = false});
|
||||
}).finally(() => this._searching = false);
|
||||
},
|
||||
|
||||
actions: {
|
||||
@ -106,7 +116,7 @@ export default Ember.Controller.extend({
|
||||
},
|
||||
|
||||
clearAll() {
|
||||
this.get('selected').clear()
|
||||
this.get('selected').clear();
|
||||
$('.fps-result input[type=checkbox]').prop('checked', false);
|
||||
},
|
||||
|
||||
@ -129,6 +139,7 @@ export default Ember.Controller.extend({
|
||||
},
|
||||
|
||||
search() {
|
||||
if (this.get("isNotValidSearchTerm")) return;
|
||||
this.search();
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,7 +33,7 @@ const HeaderController = Ember.Controller.extend({
|
||||
var params = "";
|
||||
|
||||
if (context) {
|
||||
params = `?context=${context.type}&context_id=${context.id}`;
|
||||
params = `?context=${context.type}&context_id=${context.id}&skip_context=true`;
|
||||
}
|
||||
|
||||
DiscourseURL.routeTo('/search' + params);
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
import NavigationDefaultController from 'discourse/controllers/navigation/default';
|
||||
import { setting } from 'discourse/lib/computed';
|
||||
|
||||
@ -6,8 +7,9 @@ export default NavigationDefaultController.extend({
|
||||
showingParentCategory: Em.computed.none('category.parentCategory'),
|
||||
showingSubcategoryList: Em.computed.and('subcategoryListSetting', 'showingParentCategory'),
|
||||
|
||||
navItems: function() {
|
||||
if (this.get('showingSubcategoryList')) { return []; }
|
||||
return Discourse.NavItem.buildList(this.get('category'), { noSubcategories: this.get('noSubcategories') });
|
||||
}.property('category', 'noSubcategories')
|
||||
@computed("showingSubcategoryList", "category", "noSubcategories")
|
||||
navItems(showingSubcategoryList, category, noSubcategories) {
|
||||
if (showingSubcategoryList) { return []; }
|
||||
return Discourse.NavItem.buildList(category, { noSubcategories });
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,12 +1,18 @@
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
needs: ['discovery', 'discovery/topics'],
|
||||
|
||||
categories: function() {
|
||||
@computed()
|
||||
categories() {
|
||||
return Discourse.Category.list();
|
||||
}.property(),
|
||||
},
|
||||
|
||||
navItems: function() {
|
||||
return Discourse.NavItem.buildList(null, {filterMode: this.get('filterMode')});
|
||||
}.property('filterMode')
|
||||
@computed("filterMode")
|
||||
navItems(filterMode) {
|
||||
// we don't want to show the period in the navigation bar since it's in a dropdown
|
||||
if (filterMode.indexOf("top/") === 0) { filterMode = filterMode.replace("top/", ""); }
|
||||
return Discourse.NavItem.buildList(null, { filterMode });
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { setting } from 'discourse/lib/computed';
|
||||
import CanCheckEmails from 'discourse/mixins/can-check-emails';
|
||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
|
||||
export default Ember.Controller.extend(CanCheckEmails, {
|
||||
|
||||
@ -10,18 +11,18 @@ export default Ember.Controller.extend(CanCheckEmails, {
|
||||
allowBackgrounds: setting('allow_profile_backgrounds'),
|
||||
editHistoryVisible: setting('edit_history_visible_to_public'),
|
||||
|
||||
selectedCategories: function(){
|
||||
return [].concat(this.get("model.watchedCategories"),
|
||||
this.get("model.trackedCategories"),
|
||||
this.get("model.mutedCategories"));
|
||||
}.property("model.watchedCategories", "model.trackedCategories", "model.mutedCategories"),
|
||||
@computed("model.watchedCategories", "model.trackedCategories", "model.mutedCategories")
|
||||
selectedCategories(watched, tracked, muted) {
|
||||
return [].concat(watched, tracked, muted);
|
||||
},
|
||||
|
||||
// By default we haven't saved anything
|
||||
saved: false,
|
||||
|
||||
newNameInput: null,
|
||||
|
||||
userFields: function() {
|
||||
@computed("model.user_fields.@each.value")
|
||||
userFields() {
|
||||
let siteUserFields = this.site.get('user_fields');
|
||||
if (!Ember.isEmpty(siteUserFields)) {
|
||||
const userFields = this.get('model.user_fields');
|
||||
@ -35,34 +36,37 @@ export default Ember.Controller.extend(CanCheckEmails, {
|
||||
return Ember.Object.create({ value, field });
|
||||
});
|
||||
}
|
||||
}.property('model.user_fields.@each.value'),
|
||||
},
|
||||
|
||||
cannotDeleteAccount: Em.computed.not('can_delete_account'),
|
||||
deleteDisabled: Em.computed.or('saving', 'deleting', 'cannotDeleteAccount'),
|
||||
|
||||
canEditName: setting('enable_names'),
|
||||
|
||||
nameInstructions: function() {
|
||||
@computed()
|
||||
nameInstructions() {
|
||||
return I18n.t(Discourse.SiteSettings.full_name_required ? 'user.name.instructions_required' : 'user.name.instructions');
|
||||
}.property(),
|
||||
},
|
||||
|
||||
canSelectTitle: function() {
|
||||
return this.siteSettings.enable_badges && this.get('model.has_title_badges');
|
||||
}.property('model.badge_count'),
|
||||
@computed("model.has_title_badges")
|
||||
canSelectTitle(hasTitleBadges) {
|
||||
return this.siteSettings.enable_badges && hasTitleBadges;
|
||||
},
|
||||
|
||||
canChangePassword: function() {
|
||||
@computed()
|
||||
canChangePassword() {
|
||||
return !this.siteSettings.enable_sso && this.siteSettings.enable_local_logins;
|
||||
}.property(),
|
||||
},
|
||||
|
||||
canReceiveDigest: function() {
|
||||
@computed()
|
||||
canReceiveDigest() {
|
||||
return !this.siteSettings.disable_digest_emails;
|
||||
}.property(),
|
||||
},
|
||||
|
||||
availableLocales: function() {
|
||||
return this.siteSettings.available_locales.split('|').map( function(s) {
|
||||
return {name: s, value: s};
|
||||
});
|
||||
}.property(),
|
||||
@computed()
|
||||
availableLocales() {
|
||||
return this.siteSettings.available_locales.split('|').map(s => ({ name: s, value: s }));
|
||||
},
|
||||
|
||||
digestFrequencies: [{ name: I18n.t('user.email_digests.daily'), value: 1 },
|
||||
{ name: I18n.t('user.email_digests.every_three_days'), value: 3 },
|
||||
@ -86,16 +90,16 @@ export default Ember.Controller.extend(CanCheckEmails, {
|
||||
{ name: I18n.t('user.new_topic_duration.after_2_weeks'), value: 2 * 7 * 60 * 24 },
|
||||
{ name: I18n.t('user.new_topic_duration.last_here'), value: -2 }],
|
||||
|
||||
saveButtonText: function() {
|
||||
return this.get('model.isSaving') ? I18n.t('saving') : I18n.t('save');
|
||||
}.property('model.isSaving'),
|
||||
@computed("model.isSaving")
|
||||
saveButtonText(isSaving) {
|
||||
return isSaving ? I18n.t('saving') : I18n.t('save');
|
||||
},
|
||||
|
||||
passwordProgress: null,
|
||||
|
||||
actions: {
|
||||
|
||||
save() {
|
||||
const self = this;
|
||||
this.set('saved', false);
|
||||
|
||||
const model = this.get('model');
|
||||
@ -113,28 +117,27 @@ export default Ember.Controller.extend(CanCheckEmails, {
|
||||
|
||||
// Cook the bio for preview
|
||||
model.set('name', this.get('newNameInput'));
|
||||
return model.save().then(function() {
|
||||
return model.save().then(() => {
|
||||
if (Discourse.User.currentProp('id') === model.get('id')) {
|
||||
Discourse.User.currentProp('name', model.get('name'));
|
||||
}
|
||||
model.set('bio_cooked', Discourse.Markdown.cook(Discourse.Markdown.sanitize(model.get('bio_raw'))));
|
||||
self.set('saved', true);
|
||||
this.set('saved', true);
|
||||
}).catch(popupAjaxError);
|
||||
},
|
||||
|
||||
changePassword() {
|
||||
const self = this;
|
||||
if (!this.get('passwordProgress')) {
|
||||
this.set('passwordProgress', I18n.t("user.change_password.in_progress"));
|
||||
return this.get('model').changePassword().then(function() {
|
||||
return this.get('model').changePassword().then(() => {
|
||||
// password changed
|
||||
self.setProperties({
|
||||
this.setProperties({
|
||||
changePasswordProgress: false,
|
||||
passwordProgress: I18n.t("user.change_password.success")
|
||||
});
|
||||
}, function() {
|
||||
}).catch(() => {
|
||||
// password failed to change
|
||||
self.setProperties({
|
||||
this.setProperties({
|
||||
changePasswordProgress: false,
|
||||
passwordProgress: I18n.t("user.change_password.error")
|
||||
});
|
||||
|
||||
@ -1,66 +1,61 @@
|
||||
import ModalFunctionality from 'discourse/mixins/modal-functionality';
|
||||
const BufferedProxy = window.BufferedProxy; // import BufferedProxy from 'ember-buffered-proxy/proxy';
|
||||
import binarySearch from 'discourse/lib/binary-search';
|
||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
import { on, default as computed } from "ember-addons/ember-computed-decorators";
|
||||
import Ember from 'ember';
|
||||
|
||||
const SortableArrayProxy = Ember.ArrayProxy.extend(Ember.SortableMixin);
|
||||
|
||||
export default Ember.Controller.extend(ModalFunctionality, Ember.Evented, {
|
||||
|
||||
@on('init')
|
||||
_fixOrder() {
|
||||
this.send('fixIndices');
|
||||
},
|
||||
|
||||
@computed("site.categories")
|
||||
categoriesBuffered(categories) {
|
||||
const bufProxy = Ember.ObjectProxy.extend(BufferedProxy);
|
||||
return categories.map(c => bufProxy.create({ content: c }));
|
||||
},
|
||||
|
||||
// uses propertyDidChange()
|
||||
@computed('categoriesBuffered')
|
||||
categoriesGrouped(cats) {
|
||||
const map = {};
|
||||
cats.forEach((cat) => {
|
||||
const p = cat.get('position') || 0;
|
||||
if (!map[p]) {
|
||||
map[p] = {pos: p, cats: [cat]};
|
||||
} else {
|
||||
map[p].cats.push(cat);
|
||||
categoriesOrdered: function() {
|
||||
return SortableArrayProxy.create({
|
||||
sortProperties: ['content.position'],
|
||||
content: this.get('categoriesBuffered')
|
||||
});
|
||||
}.property('categoriesBuffered'),
|
||||
|
||||
showFixIndices: function() {
|
||||
const cats = this.get('categoriesOrdered');
|
||||
const len = cats.get('length');
|
||||
for (let i = 0; i < len; i++) {
|
||||
if (cats.objectAt(i).get('position') !== i) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
const result = [];
|
||||
Object.keys(map).map(p => parseInt(p)).sort((a,b) => a-b).forEach(function(pos) {
|
||||
result.push(map[pos]);
|
||||
});
|
||||
return result;
|
||||
},
|
||||
}
|
||||
return false;
|
||||
}.property('categoriesOrdered.@each.position'),
|
||||
|
||||
showApplyAll: function() {
|
||||
let anyChanged = false;
|
||||
this.get('categoriesBuffered').forEach(bc => { anyChanged = anyChanged || bc.get('hasBufferedChanges') });
|
||||
this.get('categoriesBuffered').forEach(bc => { anyChanged = anyChanged || bc.get('hasBufferedChanges'); });
|
||||
return anyChanged;
|
||||
}.property('categoriesBuffered.@each.hasBufferedChanges'),
|
||||
|
||||
saveDisabled: Ember.computed.alias('showApplyAll'),
|
||||
saveDisabled: Ember.computed.or('showApplyAll', 'showFixIndices'),
|
||||
|
||||
moveDir(cat, dir) {
|
||||
const grouped = this.get('categoriesGrouped'),
|
||||
curPos = cat.get('position'),
|
||||
curGroupIdx = binarySearch(grouped, curPos, "pos"),
|
||||
curGroup = grouped[curGroupIdx];
|
||||
|
||||
if (curGroup.cats.length === 1 && ((dir === -1 && curGroupIdx !== 0) || (dir === 1 && curGroupIdx !== (grouped.length - 1)))) {
|
||||
const nextGroup = grouped[curGroupIdx + dir],
|
||||
nextPos = nextGroup.pos;
|
||||
cat.set('position', nextPos);
|
||||
} else {
|
||||
const cats = this.get('categoriesOrdered');
|
||||
const curIdx = cats.indexOf(cat);
|
||||
const desiredIdx = curIdx + dir;
|
||||
if (desiredIdx >= 0 && desiredIdx < cats.get('length')) {
|
||||
const curPos = cat.get('position');
|
||||
cat.set('position', curPos + dir);
|
||||
const otherCat = cats.objectAt(desiredIdx);
|
||||
otherCat.set('position', curPos - dir);
|
||||
this.send('commit');
|
||||
}
|
||||
cat.applyBufferedChanges();
|
||||
Ember.run.next(this, () => {
|
||||
this.propertyDidChange('categoriesGrouped');
|
||||
Ember.run.schedule('afterRender', this, () => {
|
||||
this.set('scrollIntoViewId', cat.get('id'));
|
||||
this.trigger('scrollIntoView');
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
actions: {
|
||||
@ -72,13 +67,22 @@ export default Ember.Controller.extend(ModalFunctionality, Ember.Evented, {
|
||||
this.moveDir(cat, 1);
|
||||
},
|
||||
|
||||
fixIndices() {
|
||||
const cats = this.get('categoriesOrdered');
|
||||
const len = cats.get('length');
|
||||
for (let i = 0; i < len; i++) {
|
||||
cats.objectAt(i).set('position', i);
|
||||
}
|
||||
this.send('commit');
|
||||
},
|
||||
|
||||
commit() {
|
||||
this.get('categoriesBuffered').forEach(bc => {
|
||||
if (bc.get('hasBufferedChanges')) {
|
||||
bc.applyBufferedChanges();
|
||||
}
|
||||
});
|
||||
this.propertyDidChange('categoriesGrouped');
|
||||
this.propertyDidChange('categoriesBuffered');
|
||||
},
|
||||
|
||||
saveOrder() {
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
// This controller supports the admin menu on topics
|
||||
export default Ember.Controller.extend({
|
||||
menuVisible: false,
|
||||
showRecover: Em.computed.and('model.deleted', 'model.details.can_recover'),
|
||||
isFeatured: Em.computed.or("model.pinned_at", "model.isBanner"),
|
||||
|
||||
actions: {
|
||||
show: function() { this.set('menuVisible', true); },
|
||||
hide: function() { this.set('menuVisible', false); }
|
||||
}
|
||||
|
||||
});
|
||||
@ -19,6 +19,10 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
||||
enteredAt: null,
|
||||
firstPostExpanded: false,
|
||||
retrying: false,
|
||||
adminMenuVisible: false,
|
||||
|
||||
showRecover: Em.computed.and('model.deleted', 'model.details.can_recover'),
|
||||
isFeatured: Em.computed.or("model.pinned_at", "model.isBanner"),
|
||||
|
||||
maxTitleLength: setting('max_topic_title_length'),
|
||||
|
||||
@ -93,6 +97,14 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
||||
}.on('init'),
|
||||
|
||||
actions: {
|
||||
showTopicAdminMenu() {
|
||||
this.set('adminMenuVisible', true);
|
||||
},
|
||||
|
||||
hideTopicAdminMenu() {
|
||||
this.set('adminMenuVisible', false);
|
||||
},
|
||||
|
||||
deleteTopic() {
|
||||
this.deleteTopic();
|
||||
},
|
||||
|
||||
@ -37,7 +37,7 @@ export default Ember.Controller.extend({
|
||||
|
||||
show(username, postId, target) {
|
||||
// XSS protection (should be encapsulated)
|
||||
username = username.toString().replace(/[^A-Za-z0-9_]/g, "");
|
||||
username = username.toString().replace(/[^A-Za-z0-9_\.\-]/g, "");
|
||||
|
||||
// Don't show on mobile
|
||||
if (Discourse.Mobile.mobileView) {
|
||||
|
||||
@ -1,37 +1,29 @@
|
||||
import registerUnbound from 'discourse/helpers/register-unbound';
|
||||
import avatarTemplate from 'discourse/lib/avatar-template';
|
||||
import { longDate, autoUpdatingRelativeAge, number } from 'discourse/lib/formatter';
|
||||
|
||||
const safe = Handlebars.SafeString;
|
||||
|
||||
Em.Handlebars.helper('bound-avatar', function(user, size, uploadId) {
|
||||
Em.Handlebars.helper('bound-avatar', (user, size) => {
|
||||
if (Em.isEmpty(user)) {
|
||||
return new safe("<div class='avatar-placeholder'></div>");
|
||||
}
|
||||
|
||||
const username = Em.get(user, 'username');
|
||||
if (arguments.length < 4) { uploadId = Em.get(user, 'uploaded_avatar_id'); }
|
||||
const avatar = Em.get(user, 'avatar_template') || avatarTemplate(username, uploadId);
|
||||
|
||||
const avatar = Em.get(user, 'avatar_template');
|
||||
return new safe(Discourse.Utilities.avatarImg({ size: size, avatarTemplate: avatar }));
|
||||
}, 'username', 'uploaded_avatar_id', 'avatar_template');
|
||||
}, 'username', 'avatar_template');
|
||||
|
||||
/*
|
||||
* Used when we only have a template
|
||||
*/
|
||||
Em.Handlebars.helper('bound-avatar-template', function(at, size) {
|
||||
Em.Handlebars.helper('bound-avatar-template', (at, size) => {
|
||||
return new safe(Discourse.Utilities.avatarImg({ size: size, avatarTemplate: at }));
|
||||
});
|
||||
|
||||
registerUnbound('raw-date', function(dt) {
|
||||
return longDate(new Date(dt));
|
||||
});
|
||||
registerUnbound('raw-date', dt => longDate(new Date(dt)));
|
||||
|
||||
registerUnbound('age-with-tooltip', function(dt) {
|
||||
return new safe(autoUpdatingRelativeAge(new Date(dt), {title: true}));
|
||||
});
|
||||
registerUnbound('age-with-tooltip', dt => new safe(autoUpdatingRelativeAge(new Date(dt), {title: true})));
|
||||
|
||||
registerUnbound('number', function(orig, params) {
|
||||
registerUnbound('number', (orig, params) => {
|
||||
orig = parseInt(orig, 10);
|
||||
if (isNaN(orig)) { orig = 0; }
|
||||
|
||||
|
||||
@ -1,24 +1,23 @@
|
||||
import registerUnbound from 'discourse/helpers/register-unbound';
|
||||
import avatarTemplate from 'discourse/lib/avatar-template';
|
||||
|
||||
function renderAvatar(user, options) {
|
||||
options = options || {};
|
||||
|
||||
if (user) {
|
||||
var username = Em.get(user, 'username');
|
||||
if (!username) {
|
||||
if (!options.usernamePath) { return ''; }
|
||||
username = Em.get(user, options.usernamePath);
|
||||
}
|
||||
|
||||
var title;
|
||||
const username = Em.get(user, options.usernamePath || 'username');
|
||||
const avatarTemplate = Em.get(user, options.avatarTemplatePath || 'avatar_template');
|
||||
|
||||
if (!username || !avatarTemplate) { return ''; }
|
||||
|
||||
let title;
|
||||
if (!options.ignoreTitle) {
|
||||
// first try to get a title
|
||||
title = Em.get(user, 'title');
|
||||
// if there was no title provided
|
||||
if (!title) {
|
||||
// try to retrieve a description
|
||||
var description = Em.get(user, 'description');
|
||||
const description = Em.get(user, 'description');
|
||||
// if a description has been provided
|
||||
if (description && description.length > 0) {
|
||||
// preprend the username before the description
|
||||
@ -27,14 +26,11 @@ function renderAvatar(user, options) {
|
||||
}
|
||||
}
|
||||
|
||||
// this is simply done to ensure we cache images correctly
|
||||
var uploadedAvatarId = Em.get(user, 'uploaded_avatar_id') || Em.get(user, 'user.uploaded_avatar_id');
|
||||
|
||||
return Discourse.Utilities.avatarImg({
|
||||
size: options.imageSize,
|
||||
extraClasses: Em.get(user, 'extras') || options.extraClasses,
|
||||
title: title || username,
|
||||
avatarTemplate: avatarTemplate(username, uploadedAvatarId)
|
||||
avatarTemplate: avatarTemplate
|
||||
});
|
||||
} else {
|
||||
return '';
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
import interceptClick from 'discourse/lib/intercept-click';
|
||||
import DiscourseURL from 'discourse/lib/url';
|
||||
|
||||
export default {
|
||||
name: "click-interceptor",
|
||||
initialize() {
|
||||
$('#main').on('click.discourse', 'a', interceptClick);
|
||||
$(window).on('hashchange', () => DiscourseURL.routeTo(document.location.hash));
|
||||
}
|
||||
};
|
||||
|
||||
@ -163,7 +163,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
uiManager = new UIManager(idPostfix, panels, undoManager, previewManager, commandManager, options.helpButton, getString);
|
||||
uiManager = new UIManager(idPostfix, panels, undoManager, previewManager, commandManager, options.helpButton, getString, options);
|
||||
uiManager.setUndoRedoButtonStates();
|
||||
|
||||
var forceRefresh = that.refreshPreview = function () { previewManager.refresh(true); };
|
||||
@ -1219,12 +1219,12 @@
|
||||
}, 0);
|
||||
};
|
||||
|
||||
function UIManager(postfix, panels, undoManager, previewManager, commandManager, helpOptions, getString) {
|
||||
function UIManager(postfix, panels, undoManager, previewManager, commandManager, helpOptions, getString, options) {
|
||||
|
||||
var inputBox = panels.input,
|
||||
buttons = {}; // buttons.undo, buttons.link, etc. The actual DOM elements.
|
||||
|
||||
makeSpritedButtonRow();
|
||||
makeSpritedButtonRow(options);
|
||||
|
||||
var keyEvent = "keydown";
|
||||
|
||||
@ -1396,7 +1396,8 @@
|
||||
return function () { method.apply(commandManager, arguments); }
|
||||
}
|
||||
|
||||
function makeSpritedButtonRow() {
|
||||
function makeSpritedButtonRow(options) {
|
||||
options = options || {};
|
||||
|
||||
var buttonBar = panels.buttonBar;
|
||||
var buttonRow = document.createElement("div");
|
||||
@ -1459,17 +1460,21 @@
|
||||
buttons.heading = makeButton("wmd-heading-button", getString("heading"), bindCommand("doHeading"));
|
||||
buttons.hr = makeButton("wmd-hr-button", getString("hr"), bindCommand("doHorizontalRule"));
|
||||
|
||||
// If we have any buttons to append, do it!
|
||||
if (typeof PagedownCustom != "undefined") {
|
||||
var appendButtons = PagedownCustom.appendButtons
|
||||
if (appendButtons && (appendButtons.length > 0)) {
|
||||
for (var i=0; i< appendButtons.length; i++) {
|
||||
var b = appendButtons[i];
|
||||
function createExtraButtons(buttons) {
|
||||
if (buttons && (buttons.length > 0)) {
|
||||
for (var i=0; i< buttons.length; i++) {
|
||||
var b = buttons[i];
|
||||
makeButton(b.id, b.description, b.execute)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we have any buttons to append, do it!
|
||||
if (typeof PagedownCustom != "undefined") {
|
||||
createExtraButtons(PagedownCustom.appendButtons);
|
||||
}
|
||||
|
||||
createExtraButtons(options.appendButtons);
|
||||
|
||||
//makeSpacer(3);
|
||||
//buttons.undo = makeButton("wmd-undo-button", getString("undo"), null);
|
||||
|
||||
@ -1,32 +0,0 @@
|
||||
import { hashString } from 'discourse/lib/hash';
|
||||
|
||||
let _splitAvatars;
|
||||
|
||||
function defaultAvatar(username) {
|
||||
const defaultAvatars = Discourse.SiteSettings.default_avatars;
|
||||
if (defaultAvatars && defaultAvatars.length) {
|
||||
_splitAvatars = _splitAvatars || defaultAvatars.split("\n");
|
||||
|
||||
if (_splitAvatars.length) {
|
||||
const hash = hashString(username);
|
||||
return _splitAvatars[Math.abs(hash) % _splitAvatars.length];
|
||||
}
|
||||
}
|
||||
|
||||
return Discourse.getURLWithCDN("/letter_avatar/" +
|
||||
username.toLowerCase() +
|
||||
"/{size}/" +
|
||||
Discourse.LetterAvatarVersion + ".png");
|
||||
}
|
||||
|
||||
export default function(username, uploadedAvatarId) {
|
||||
if (uploadedAvatarId) {
|
||||
return Discourse.getURLWithCDN("/user_avatar/" +
|
||||
Discourse.BaseUrl +
|
||||
"/" +
|
||||
username.toLowerCase() +
|
||||
"/{size}/" +
|
||||
uploadedAvatarId + ".png");
|
||||
}
|
||||
return defaultAvatar(username);
|
||||
}
|
||||
@ -22,4 +22,4 @@ Discourse.CensoredWords = {
|
||||
}
|
||||
return text;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
function exportEntityByType(type, entity) {
|
||||
function exportEntityByType(type, entity, args) {
|
||||
return Discourse.ajax("/export_csv/export_entity.json", {
|
||||
method: 'POST',
|
||||
data: {entity_type: type, entity}
|
||||
data: {entity_type: type, entity, args}
|
||||
});
|
||||
}
|
||||
|
||||
@ -14,6 +14,6 @@ export function exportUserArchive() {
|
||||
}
|
||||
|
||||
|
||||
export function exportEntity(entity) {
|
||||
return exportEntityByType('admin', entity);
|
||||
export function exportEntity(entity, args) {
|
||||
return exportEntityByType('admin', entity, args);
|
||||
}
|
||||
|
||||
@ -14,7 +14,7 @@ try {
|
||||
|
||||
const KeyValueStore = function(ctx) {
|
||||
this.context = ctx;
|
||||
}
|
||||
};
|
||||
|
||||
KeyValueStore.prototype = {
|
||||
abandonLocal() {
|
||||
@ -32,6 +32,7 @@ KeyValueStore.prototype = {
|
||||
},
|
||||
|
||||
remove(key) {
|
||||
if (!safeLocalStorage) { return; }
|
||||
return safeLocalStorage.removeItem(this.context + key);
|
||||
},
|
||||
|
||||
|
||||
@ -1,62 +1,53 @@
|
||||
import DiscourseURL from 'discourse/lib/url';
|
||||
|
||||
const PATH_BINDINGS = {
|
||||
'g h': '/',
|
||||
'g l': '/latest',
|
||||
'g n': '/new',
|
||||
'g u': '/unread',
|
||||
'g c': '/categories',
|
||||
'g t': '/top',
|
||||
'g b': '/bookmarks',
|
||||
'g p': '/my/activity',
|
||||
'g m': '/my/messages'
|
||||
},
|
||||
|
||||
SELECTED_POST_BINDINGS = {
|
||||
'd': 'deletePost',
|
||||
'e': 'editPost',
|
||||
'l': 'toggleLike',
|
||||
'r': 'replyToPost',
|
||||
'!': 'showFlags',
|
||||
't': 'replyAsNewTopic'
|
||||
},
|
||||
|
||||
CLICK_BINDINGS = {
|
||||
'm m': 'div.notification-options li[data-id="0"] a', // mark topic as muted
|
||||
'm r': 'div.notification-options li[data-id="1"] a', // mark topic as regular
|
||||
'm t': 'div.notification-options li[data-id="2"] a', // mark topic as tracking
|
||||
'm w': 'div.notification-options li[data-id="3"] a', // mark topic as watching
|
||||
'x r': '#dismiss-new,#dismiss-new-top,#dismiss-posts,#dismiss-posts-top', // dismiss new/posts
|
||||
'x t': '#dismiss-topics,#dismiss-topics-top', // dismiss topics
|
||||
'.': '.alert.alert-info.clickable', // show incoming/updated topics
|
||||
'o,enter': '.topic-list tr.selected a.title', // open selected topic
|
||||
'shift+s': '#topic-footer-buttons button.share', // share topic
|
||||
's': '.topic-post.selected a.post-date' // share post
|
||||
},
|
||||
|
||||
FUNCTION_BINDINGS = {
|
||||
'c': 'createTopic', // create new topic
|
||||
'home': 'goToFirstPost',
|
||||
'#': 'toggleProgress',
|
||||
'end': 'goToLastPost',
|
||||
'shift+j': 'nextSection',
|
||||
'j': 'selectDown',
|
||||
'shift+k': 'prevSection',
|
||||
'shift+p': 'pinUnpinTopic',
|
||||
'k': 'selectUp',
|
||||
'u': 'goBack',
|
||||
'/': 'showSearch',
|
||||
'=': 'toggleHamburgerMenu',
|
||||
'p': 'showCurrentUser', // open current user menu
|
||||
'ctrl+f': 'showBuiltinSearch',
|
||||
'command+f': 'showBuiltinSearch',
|
||||
'?': 'showHelpModal', // open keyboard shortcut help
|
||||
'q': 'quoteReply',
|
||||
'b': 'toggleBookmark',
|
||||
'f': 'toggleBookmarkTopic',
|
||||
'shift+r': 'replyToTopic',
|
||||
'shift+z shift+z': 'logout'
|
||||
};
|
||||
const bindings = {
|
||||
'!': {postAction: 'showFlags'},
|
||||
'#': {handler: 'toggleProgress', anonymous: true},
|
||||
'/': {handler: 'showSearch', anonymous: true},
|
||||
'=': {handler: 'toggleHamburgerMenu', anonymous: true},
|
||||
'?': {handler: 'showHelpModal', anonymous: true},
|
||||
'.': {click: '.alert.alert-info.clickable', anonymous: true}, // show incoming/updated topics
|
||||
'b': {handler: 'toggleBookmark'},
|
||||
'c': {handler: 'createTopic'},
|
||||
'ctrl+f': {handler: 'showBuiltinSearch', anonymous: true},
|
||||
'command+f': {handler: 'showBuiltinSearch', anonymous: true},
|
||||
'd': {postAction: 'deletePost'},
|
||||
'e': {postAction: 'editPost'},
|
||||
'end': {handler: 'goToLastPost', anonymous: true},
|
||||
'f': {handler: 'toggleBookmarkTopic'},
|
||||
'g h': {path: '/', anonymous: true},
|
||||
'g l': {path: '/latest', anonymous: true},
|
||||
'g n': {path: '/new'},
|
||||
'g u': {path: '/unread'},
|
||||
'g c': {path: '/categories', anonymous: true},
|
||||
'g t': {path: '/top', anonymous: true},
|
||||
'g b': {path: '/bookmarks'},
|
||||
'g p': {path: '/my/activity'},
|
||||
'g m': {path: '/my/messages'},
|
||||
'home': {handler: 'goToFirstPost', anonymous: true},
|
||||
'j': {handler: 'selectDown', anonymous: true},
|
||||
'k': {handler: 'selectUp', anonymous: true},
|
||||
'l': {postAction: 'toggleLike'},
|
||||
'm m': {click: 'div.notification-options li[data-id="0"] a'}, // mark topic as muted
|
||||
'm r': {click: 'div.notification-options li[data-id="1"] a'}, // mark topic as regular
|
||||
'm t': {click: 'div.notification-options li[data-id="2"] a'}, // mark topic as tracking
|
||||
'm w': {click: 'div.notification-options li[data-id="3"] a'}, // mark topic as watching
|
||||
'o,enter': {click: '.topic-list tr.selected a.title', anonymous: true}, // open selected topic
|
||||
'p': {handler: 'showCurrentUser'},
|
||||
'q': {handler: 'quoteReply'},
|
||||
'r': {postAction: 'replyToPost'},
|
||||
's': {click: '.topic-post.selected a.post-date', anonymous: true}, // share post
|
||||
'shift+j': {handler: 'nextSection', anonymous: true},
|
||||
'shift+k': {handler: 'prevSection', anonymous: true},
|
||||
'shift+p': {handler: 'pinUnpinTopic'},
|
||||
'shift+r': {handler: 'replyToTopic'},
|
||||
'shift+s': {click: '#topic-footer-buttons button.share', anonymous: true}, // share topic
|
||||
'shift+z shift+z': {handler: 'logout'},
|
||||
't': {postAction: 'replyAsNewTopic'},
|
||||
'u': {handler: 'goBack', anonymous: true},
|
||||
'x r': {click: '#dismiss-new,#dismiss-new-top,#dismiss-posts,#dismiss-posts-top'}, // dismiss new/posts
|
||||
'x t': {click: '#dismiss-topics,#dismiss-topics-top'} // dismiss topics
|
||||
};
|
||||
|
||||
|
||||
export default {
|
||||
@ -65,14 +56,24 @@ export default {
|
||||
this.container = container;
|
||||
this._stopCallback();
|
||||
|
||||
|
||||
this.searchService = this.container.lookup('search-service:main');
|
||||
this.appEvents = this.container.lookup('app-events:main');
|
||||
this.currentUser = this.container.lookup('current-user:main');
|
||||
|
||||
_.each(PATH_BINDINGS, this._bindToPath, this);
|
||||
_.each(CLICK_BINDINGS, this._bindToClick, this);
|
||||
_.each(SELECTED_POST_BINDINGS, this._bindToSelectedPost, this);
|
||||
_.each(FUNCTION_BINDINGS, this._bindToFunction, this);
|
||||
Object.keys(bindings).forEach(key => {
|
||||
const binding = bindings[key];
|
||||
if (!binding.anonymous && !this.currentUser) { return; }
|
||||
|
||||
if (binding.path) {
|
||||
this._bindToPath(binding.path, key);
|
||||
} else if (binding.handler) {
|
||||
this._bindToFunction(binding.handler, key);
|
||||
} else if (binding.postAction) {
|
||||
this._bindToSelectedPost(binding.postAction, key);
|
||||
} else if (binding.click) {
|
||||
this._bindToClick(binding.click, key);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
toggleBookmark() {
|
||||
@ -223,17 +224,11 @@ export default {
|
||||
},
|
||||
|
||||
_bindToSelectedPost(action, binding) {
|
||||
const self = this;
|
||||
|
||||
this.keyTrapper.bind(binding, function() {
|
||||
self.sendToSelectedPost(action);
|
||||
});
|
||||
this.keyTrapper.bind(binding, () => this.sendToSelectedPost(action));
|
||||
},
|
||||
|
||||
_bindToPath(path, binding) {
|
||||
this.keyTrapper.bind(binding, function() {
|
||||
DiscourseURL.routeTo(path);
|
||||
});
|
||||
_bindToPath(path, key) {
|
||||
this.keyTrapper.bind(key, () => DiscourseURL.routeTo(path));
|
||||
},
|
||||
|
||||
_bindToClick(selector, binding) {
|
||||
|
||||
@ -152,16 +152,16 @@ Discourse.Markdown = {
|
||||
return this.markdownConverter(opts).makeHtml(raw);
|
||||
},
|
||||
|
||||
createEditor: function(converterOptions) {
|
||||
if (!converterOptions) converterOptions = {};
|
||||
createEditor: function(options) {
|
||||
options = options || {};
|
||||
|
||||
// By default we always sanitize content in the editor
|
||||
converterOptions.sanitize = true;
|
||||
options.sanitize = true;
|
||||
|
||||
var markdownConverter = Discourse.Markdown.markdownConverter(converterOptions);
|
||||
var markdownConverter = Discourse.Markdown.markdownConverter(options);
|
||||
|
||||
var editorOptions = {
|
||||
containerElement: converterOptions.containerElement,
|
||||
containerElement: options.containerElement,
|
||||
strings: {
|
||||
bold: I18n.t("composer.bold_title") + " <strong> Ctrl+B",
|
||||
boldexample: I18n.t("composer.bold_text"),
|
||||
@ -197,7 +197,8 @@ Discourse.Markdown = {
|
||||
redomac: I18n.t("composer.redo_title") + " - Ctrl+Shift+Z",
|
||||
|
||||
help: I18n.t("composer.help")
|
||||
}
|
||||
},
|
||||
appendButtons: options.appendButtons
|
||||
};
|
||||
|
||||
return new Markdown.Editor(markdownConverter, undefined, editorOptions);
|
||||
|
||||
@ -1,11 +1,8 @@
|
||||
function applicable() {
|
||||
|
||||
// CriOS is Chrome on iPad / iPhone, OPiOS is Opera (they need no patching)
|
||||
// Dolphin has a wierd user agent, rest seem a bit nitch
|
||||
// This will apply hack on all iDevices
|
||||
return navigator.userAgent.match(/(iPad|iPhone|iPod)/g) &&
|
||||
navigator.userAgent.match(/Safari/g) &&
|
||||
!navigator.userAgent.match(/CriOS/g) &&
|
||||
!navigator.userAgent.match(/OPiOS/g);
|
||||
navigator.userAgent.match(/Safari/g);
|
||||
}
|
||||
|
||||
// per http://stackoverflow.com/questions/29001977/safari-in-ios8-is-scrolling-screen-when-fixed-elements-get-focus/29064810
|
||||
@ -17,6 +14,8 @@ function positioningWorkaround($fixedElement) {
|
||||
const fixedElement = $fixedElement[0];
|
||||
|
||||
var done = false;
|
||||
var originalScrollTop = 0;
|
||||
var wasDocked;
|
||||
|
||||
var blurredNow = function(evt) {
|
||||
if (!done && _.include($(document.activeElement).parents(), fixedElement)) {
|
||||
@ -25,8 +24,20 @@ function positioningWorkaround($fixedElement) {
|
||||
}
|
||||
|
||||
done = true;
|
||||
|
||||
fixedElement.parentElement.style.height = '';
|
||||
$('#main-outlet').show();
|
||||
$('header').show();
|
||||
|
||||
fixedElement.style.position = '';
|
||||
fixedElement.style.top = '';
|
||||
fixedElement.style.height = '';
|
||||
$(window).scrollTop(originalScrollTop);
|
||||
|
||||
if (wasDocked) {
|
||||
$('body').addClass('docked');
|
||||
}
|
||||
|
||||
if (evt) {
|
||||
evt.target.removeEventListener('blur', blurred);
|
||||
}
|
||||
@ -50,31 +61,25 @@ function positioningWorkaround($fixedElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
originalScrollTop = $(window).scrollTop();
|
||||
|
||||
wasDocked = $('body').hasClass('docked');
|
||||
|
||||
// take care of body
|
||||
$('#main-outlet').hide();
|
||||
$('header').hide();
|
||||
|
||||
|
||||
fixedElement.style.position = 'absolute';
|
||||
// get out of the way while opening keyboard
|
||||
fixedElement.style.top = '0px';
|
||||
fixedElement.style.height = parseInt(window.innerHeight*0.6) + "px";
|
||||
fixedElement.parentElement.style.height = window.innerHeight + "px";
|
||||
$(window).scrollTop(0);
|
||||
// great ... iOS positions this yet again
|
||||
// so lets take over if this happens
|
||||
setTimeout(()=>$(window).scrollTop(0),500);
|
||||
|
||||
var iPadOffset = 0;
|
||||
if (window.innerHeight > window.innerWidth && navigator.userAgent.match(/iPad/)) {
|
||||
// there is no way to get virtual keyboard height
|
||||
iPadOffset = 640 - $(fixedElement).height();
|
||||
}
|
||||
|
||||
var oldScrollY = 0;
|
||||
|
||||
var positionElement = function(){
|
||||
if (done) {
|
||||
return;
|
||||
}
|
||||
if (Math.abs(oldScrollY - window.scrollY) < 20) {
|
||||
return;
|
||||
}
|
||||
oldScrollY = window.scrollY;
|
||||
fixedElement.style.top = window.scrollY + iPadOffset + 'px';
|
||||
};
|
||||
|
||||
// position once, correctly, after keyboard is shown
|
||||
setTimeout(positionElement, 500);
|
||||
|
||||
evt.preventDefault();
|
||||
self.focus();
|
||||
@ -88,7 +93,11 @@ function positioningWorkaround($fixedElement) {
|
||||
}
|
||||
|
||||
const checkForInputs = _.debounce(function(){
|
||||
$fixedElement.find('button,a').each(function(){
|
||||
$fixedElement.find('button,a:not(.autocomplete)').each(function(idx, elem){
|
||||
if ($(elem).parents('.autocomplete').length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
attachTouchStart(this, function(evt){
|
||||
done = true;
|
||||
$(document.activeElement).blur();
|
||||
|
||||
@ -30,7 +30,7 @@ const ScreenTrack = Ember.Object.extend({
|
||||
self.tick();
|
||||
}, 1000));
|
||||
|
||||
$(window).on('scroll.screentrack', function(){self.scrolled()});
|
||||
$(window).on('scroll.screentrack', function(){self.scrolled();});
|
||||
}
|
||||
|
||||
this.set('topicId', topicId);
|
||||
|
||||
@ -103,7 +103,15 @@ const searchContextDescription = function(type, name){
|
||||
|
||||
const getSearchKey = function(args){
|
||||
return args.q + "|" + ((args.searchContext && args.searchContext.type) || "") + "|" +
|
||||
((args.searchContext && args.searchContext.id) || "")
|
||||
((args.searchContext && args.searchContext.id) || "");
|
||||
};
|
||||
|
||||
export { searchForTerm, searchContextDescription, getSearchKey };
|
||||
const isValidSearchTerm = function(searchTerm) {
|
||||
if (searchTerm) {
|
||||
return searchTerm.trim().length >= Discourse.SiteSettings.min_search_term_length;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export { searchForTerm, searchContextDescription, getSearchKey, isValidSearchTerm };
|
||||
|
||||
@ -105,7 +105,7 @@ const DiscourseURL = Ember.Object.createWithMixins({
|
||||
It contains the logic necessary to route within a topic using replaceState to
|
||||
keep the history intact.
|
||||
**/
|
||||
routeTo: function(path, opts) {
|
||||
routeTo(path, opts) {
|
||||
if (Em.isEmpty(path)) { return; }
|
||||
|
||||
if (Discourse.get('requiresRefresh')) {
|
||||
@ -122,6 +122,7 @@ const DiscourseURL = Ember.Object.createWithMixins({
|
||||
// Scroll to the same page, different anchor
|
||||
if (path.indexOf('#') === 0) {
|
||||
this.scrollToId(path);
|
||||
history.replaceState(undefined, undefined, path);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -271,7 +272,7 @@ const DiscourseURL = Ember.Object.createWithMixins({
|
||||
|
||||
// This has been extracted so it can be tested.
|
||||
origin: function() {
|
||||
return window.location.origin;
|
||||
return window.location.origin + (Discourse.BaseUri === "/" ? '' : Discourse.BaseUri);
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@ -99,7 +99,7 @@ Discourse.Utilities = {
|
||||
div.innerHTML = html;
|
||||
var $div = $(div);
|
||||
// Find all emojis and replace with its title attribute.
|
||||
$div.find('img.emoji').replaceWith(function() { return this.title });
|
||||
$div.find('img.emoji').replaceWith(function() { return this.title; });
|
||||
$('.clicks', $div).remove();
|
||||
var text = div.textContent || div.innerText || "";
|
||||
|
||||
@ -215,6 +215,10 @@ Discourse.Utilities = {
|
||||
}
|
||||
},
|
||||
|
||||
getUploadPlaceholder: function(filename) {
|
||||
return "[" + I18n.t("uploading_filename", { filename: filename }) + "]() ";
|
||||
},
|
||||
|
||||
isAnImage: function(path) {
|
||||
return (/\.(png|jpe?g|gif|bmp|tiff?|svg|webp)$/i).test(path);
|
||||
},
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import Eyeline from 'discourse/lib/eyeline';
|
||||
import Scrolling from 'discourse/mixins/scrolling';
|
||||
import { on } from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
// Provides the ability to load more items for a view which is scrolled to the bottom.
|
||||
export default Ember.Mixin.create(Ember.ViewTargetActionSupport, Scrolling, {
|
||||
@ -9,15 +10,23 @@ export default Ember.Mixin.create(Ember.ViewTargetActionSupport, Scrolling, {
|
||||
if (eyeline) { eyeline.update(); }
|
||||
},
|
||||
|
||||
_bindEyeline: function() {
|
||||
loadMoreUnlessFull() {
|
||||
if (this.screenNotFull()) {
|
||||
this.send("loadMore");
|
||||
}
|
||||
},
|
||||
|
||||
@on("didInsertElement")
|
||||
_bindEyeline() {
|
||||
const eyeline = new Eyeline(this.get('eyelineSelector') + ":last");
|
||||
this.set('eyeline', eyeline);
|
||||
eyeline.on('sawBottom', () => this.send('loadMore'));
|
||||
this.bindScrolling();
|
||||
}.on('didInsertElement'),
|
||||
},
|
||||
|
||||
_removeEyeline: function() {
|
||||
@on("willDestroyElement")
|
||||
_removeEyeline() {
|
||||
this.unbindScrolling();
|
||||
}.on('willDestroyElement')
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@ -3,7 +3,7 @@ export default Em.Mixin.create({
|
||||
|
||||
needs: ['modal'],
|
||||
|
||||
flash: function(message, messageClass) {
|
||||
flash(message, messageClass) {
|
||||
this.set('flashMessage', Em.Object.create({ message, messageClass }));
|
||||
}
|
||||
});
|
||||
|
||||
@ -6,16 +6,20 @@ import debounce from 'discourse/lib/debounce';
|
||||
easier.
|
||||
**/
|
||||
const ScrollingDOMMethods = {
|
||||
bindOnScroll: function(onScrollMethod, name) {
|
||||
bindOnScroll(onScrollMethod, name) {
|
||||
name = name || 'default';
|
||||
$(document).bind('touchmove.discourse-' + name, onScrollMethod);
|
||||
$(window).bind('scroll.discourse-' + name, onScrollMethod);
|
||||
$(document).bind(`touchmove.discourse-${name}`, onScrollMethod);
|
||||
$(window).bind(`scroll.discourse-${name}`, onScrollMethod);
|
||||
},
|
||||
|
||||
unbindOnScroll: function(name) {
|
||||
unbindOnScroll(name) {
|
||||
name = name || 'default';
|
||||
$(window).unbind('scroll.discourse-' + name);
|
||||
$(document).unbind('touchmove.discourse-' + name);
|
||||
$(window).unbind(`scroll.discourse-${name}`);
|
||||
$(document).unbind(`touchmove.discourse-${name}`);
|
||||
},
|
||||
|
||||
screenNotFull() {
|
||||
return $(window).height() >= $(document).height();
|
||||
}
|
||||
};
|
||||
|
||||
@ -23,16 +27,15 @@ const Scrolling = Ember.Mixin.create({
|
||||
|
||||
// Begin watching for scroll events. By default they will be called at max every 100ms.
|
||||
// call with {debounce: N} for a diff time
|
||||
bindScrolling: function(opts) {
|
||||
opts = opts || {debounce: 100};
|
||||
bindScrolling(opts) {
|
||||
opts = opts || { debounce: 100 };
|
||||
|
||||
// So we can not call the scrolled event while transitioning
|
||||
const router = Discourse.__container__.lookup('router:main').router;
|
||||
|
||||
const self = this;
|
||||
var onScrollMethod = function() {
|
||||
let onScrollMethod = () => {
|
||||
if (router.activeTransition) { return; }
|
||||
return Em.run.scheduleOnce('afterRender', self, 'scrolled');
|
||||
return Ember.run.scheduleOnce('afterRender', this, 'scrolled');
|
||||
};
|
||||
|
||||
if (opts.debounce) {
|
||||
@ -40,10 +43,11 @@ const Scrolling = Ember.Mixin.create({
|
||||
}
|
||||
|
||||
ScrollingDOMMethods.bindOnScroll(onScrollMethod, opts.name);
|
||||
Em.run.scheduleOnce('afterRender', onScrollMethod);
|
||||
},
|
||||
|
||||
unbindScrolling: function(name) {
|
||||
screenNotFull: () => ScrollingDOMMethods.screenNotFull(),
|
||||
|
||||
unbindScrolling(name) {
|
||||
ScrollingDOMMethods.unbindOnScroll(name);
|
||||
}
|
||||
});
|
||||
|
||||
@ -10,7 +10,7 @@ export default {
|
||||
|
||||
findStale(store, type, findArgs, opts) {
|
||||
const staleResult = new StaleResult();
|
||||
const key = (opts && opts.storageKey) || this.storageKey(type, findArgs)
|
||||
const key = (opts && opts.storageKey) || this.storageKey(type, findArgs);
|
||||
try {
|
||||
const stored = this.keyValueStore.getItem(key);
|
||||
if (stored) {
|
||||
@ -24,11 +24,11 @@ export default {
|
||||
},
|
||||
|
||||
find(store, type, findArgs, opts) {
|
||||
const key = (opts && opts.storageKey) || this.storageKey(type, findArgs)
|
||||
const key = (opts && opts.storageKey) || this.storageKey(type, findArgs);
|
||||
|
||||
return this._super(store, type, findArgs).then((results) => {
|
||||
this.keyValueStore.setItem(key, JSON.stringify(results));
|
||||
return results;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -198,76 +198,71 @@ var _uncategorized;
|
||||
|
||||
Category.reopenClass({
|
||||
|
||||
findUncategorized: function() {
|
||||
findUncategorized() {
|
||||
_uncategorized = _uncategorized || Category.list().findBy('id', Discourse.Site.currentProp('uncategorized_category_id'));
|
||||
return _uncategorized;
|
||||
},
|
||||
|
||||
slugFor: function(category) {
|
||||
slugFor(category) {
|
||||
if (!category) return "";
|
||||
|
||||
var parentCategory = Em.get(category, 'parentCategory'),
|
||||
result = "";
|
||||
const parentCategory = Em.get(category, 'parentCategory');
|
||||
let result = "";
|
||||
|
||||
if (parentCategory) {
|
||||
result = Category.slugFor(parentCategory) + "/";
|
||||
}
|
||||
|
||||
var id = Em.get(category, 'id'),
|
||||
slug = Em.get(category, 'slug');
|
||||
const id = Em.get(category, 'id'),
|
||||
slug = Em.get(category, 'slug');
|
||||
|
||||
if (!slug || slug.trim().length === 0) return result + id + "-category";
|
||||
return result + slug;
|
||||
return !slug || slug.trim().length === 0 ? `${result}${id}-category` : result + slug;
|
||||
},
|
||||
|
||||
list: function() {
|
||||
if (Discourse.SiteSettings.fixed_category_positions) {
|
||||
return Discourse.Site.currentProp('categories');
|
||||
} else {
|
||||
return Discourse.Site.currentProp('sortedCategories');
|
||||
}
|
||||
list() {
|
||||
return Discourse.SiteSettings.fixed_category_positions ?
|
||||
Discourse.Site.currentProp('categories') :
|
||||
Discourse.Site.currentProp('sortedCategories');
|
||||
},
|
||||
|
||||
listByActivity: function() {
|
||||
listByActivity() {
|
||||
return Discourse.Site.currentProp('sortedCategories');
|
||||
},
|
||||
|
||||
idMap: function() {
|
||||
idMap() {
|
||||
return Discourse.Site.currentProp('categoriesById');
|
||||
},
|
||||
|
||||
findSingleBySlug: function(slug) {
|
||||
return Category.list().find(function(c) {
|
||||
return Category.slugFor(c) === slug;
|
||||
});
|
||||
findSingleBySlug(slug) {
|
||||
return Category.list().find(c => Category.slugFor(c) === slug);
|
||||
},
|
||||
|
||||
findById: function(id) {
|
||||
findById(id) {
|
||||
if (!id) { return; }
|
||||
return Category.idMap()[id];
|
||||
},
|
||||
|
||||
findByIds: function(ids){
|
||||
var categories = [];
|
||||
_.each(ids, function(id){
|
||||
var found = Category.findById(id);
|
||||
if(found){
|
||||
findByIds(ids) {
|
||||
const categories = [];
|
||||
_.each(ids, id => {
|
||||
const found = Category.findById(id);
|
||||
if (found) {
|
||||
categories.push(found);
|
||||
}
|
||||
});
|
||||
return categories;
|
||||
},
|
||||
|
||||
findBySlug: function(slug, parentSlug) {
|
||||
var categories = Category.list(),
|
||||
category;
|
||||
findBySlug(slug, parentSlug) {
|
||||
const categories = Category.list();
|
||||
let category;
|
||||
|
||||
if (parentSlug) {
|
||||
var parentCategory = Category.findSingleBySlug(parentSlug);
|
||||
const parentCategory = Category.findSingleBySlug(parentSlug);
|
||||
if (parentCategory) {
|
||||
if (slug === 'none') { return parentCategory; }
|
||||
|
||||
category = categories.find(function(item) {
|
||||
category = categories.find(item => {
|
||||
return item && item.get('parentCategory') === parentCategory && Category.slugFor(item) === (parentSlug + "/" + slug);
|
||||
});
|
||||
}
|
||||
@ -287,7 +282,7 @@ Category.reopenClass({
|
||||
},
|
||||
|
||||
reloadById(id) {
|
||||
return Discourse.ajax("/c/" + id + "/show.json");
|
||||
return Discourse.ajax(`/c/${id}/show.json`);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@ import Topic from 'discourse/models/topic';
|
||||
import { throwAjaxError } from 'discourse/lib/ajax-error';
|
||||
import Quote from 'discourse/lib/quote';
|
||||
import Draft from 'discourse/models/draft';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
const CLOSED = 'closed',
|
||||
SAVING = 'saving',
|
||||
@ -23,6 +24,7 @@ const CLOSED = 'closed',
|
||||
category: 'categoryId',
|
||||
topic_id: 'topic.id',
|
||||
is_warning: 'isWarning',
|
||||
whisper: 'whisper',
|
||||
archetype: 'archetypeId',
|
||||
target_usernames: 'targetUsernames',
|
||||
typing_duration_msecs: 'typingTime',
|
||||
@ -35,15 +37,42 @@ const CLOSED = 'closed',
|
||||
};
|
||||
|
||||
const Composer = RestModel.extend({
|
||||
_categoryId: null,
|
||||
|
||||
archetypes: function() {
|
||||
return this.site.get('archetypes');
|
||||
}.property(),
|
||||
|
||||
|
||||
@computed
|
||||
categoryId: {
|
||||
get() { return this._categoryId; },
|
||||
|
||||
// We wrap categoryId this way so we can fire `applyTopicTemplate` with
|
||||
// the previous value as well as the new value
|
||||
set(categoryId) {
|
||||
const oldCategoryId = this._categoryId;
|
||||
|
||||
if (Ember.isEmpty(categoryId)) { categoryId = null; }
|
||||
this._categoryId = categoryId;
|
||||
|
||||
if (oldCategoryId !== categoryId) {
|
||||
this.applyTopicTemplate(oldCategoryId, categoryId);
|
||||
}
|
||||
return categoryId;
|
||||
}
|
||||
},
|
||||
|
||||
creatingTopic: Em.computed.equal('action', CREATE_TOPIC),
|
||||
creatingPrivateMessage: Em.computed.equal('action', PRIVATE_MESSAGE),
|
||||
notCreatingPrivateMessage: Em.computed.not('creatingPrivateMessage'),
|
||||
|
||||
showCategoryChooser: function(){
|
||||
const manyCategories = Discourse.Category.list().length > 1;
|
||||
const hasOptions = this.get('archetype.hasOptions');
|
||||
return !this.get('privateMessage') && (hasOptions || manyCategories);
|
||||
}.property('privateMessage'),
|
||||
|
||||
privateMessage: function(){
|
||||
return this.get('creatingPrivateMessage') || this.get('topic.archetype') === 'private_message';
|
||||
}.property('creatingPrivateMessage', 'topic'),
|
||||
@ -56,6 +85,7 @@ const Composer = RestModel.extend({
|
||||
viewOpen: Em.computed.equal('composeState', OPEN),
|
||||
viewDraft: Em.computed.equal('composeState', DRAFT),
|
||||
|
||||
|
||||
composeStateChanged: function() {
|
||||
var oldOpen = this.get('composerOpened');
|
||||
|
||||
@ -339,20 +369,24 @@ const Composer = RestModel.extend({
|
||||
this.keyValueStore.set({ key: 'composer.showPreview', value: this.get('showPreview') });
|
||||
},
|
||||
|
||||
applyTopicTemplate: function() {
|
||||
applyTopicTemplate(oldCategoryId, categoryId) {
|
||||
if (this.get('action') !== CREATE_TOPIC) { return; }
|
||||
if (!Ember.isEmpty(this.get('reply'))) { return; }
|
||||
let reply = this.get('reply');
|
||||
|
||||
const categoryId = this.get('categoryId');
|
||||
const category = this.site.categories.find((c) => c.get('id') === categoryId);
|
||||
if (category) {
|
||||
const topicTemplate = category.get('topic_template');
|
||||
if (!Ember.isEmpty(topicTemplate)) {
|
||||
this.set('reply', topicTemplate);
|
||||
// If the user didn't change the template, clear it
|
||||
if (oldCategoryId) {
|
||||
const oldCat = this.site.categories.findProperty('id', oldCategoryId);
|
||||
if (oldCat && (oldCat.get('topic_template') === reply)) {
|
||||
reply = "";
|
||||
}
|
||||
}
|
||||
|
||||
}.observes('categoryId'),
|
||||
if (!Ember.isEmpty(reply)) { return; }
|
||||
const category = this.site.categories.findProperty('id', categoryId);
|
||||
if (category) {
|
||||
this.set('reply', category.get('topic_template') || "");
|
||||
}
|
||||
},
|
||||
|
||||
/*
|
||||
Open a composer
|
||||
@ -397,14 +431,22 @@ const Composer = RestModel.extend({
|
||||
}
|
||||
}
|
||||
|
||||
const categoryId = opts.categoryId || this.get('topic.category.id');
|
||||
this.setProperties({
|
||||
categoryId,
|
||||
archetypeId: opts.archetypeId || this.site.get('default_archetype'),
|
||||
metaData: opts.metaData ? Em.Object.create(opts.metaData) : null,
|
||||
reply: opts.reply || this.get("reply") || ""
|
||||
});
|
||||
|
||||
// We set the category id separately for topic templates on opening of composer
|
||||
this.set('categoryId', opts.categoryId || this.get('topic.category.id'));
|
||||
|
||||
if (!this.get('categoryId') && this.get('creatingTopic')) {
|
||||
const categories = Discourse.Category.list();
|
||||
if (categories.length === 1) {
|
||||
this.set('categoryId', categories[0].get('id'));
|
||||
}
|
||||
}
|
||||
|
||||
if (opts.postId) {
|
||||
this.set('loading', true);
|
||||
this.store.find('post', opts.postId).then(function(post) {
|
||||
@ -529,6 +571,9 @@ const Composer = RestModel.extend({
|
||||
|
||||
let addedToStream = false;
|
||||
|
||||
const postTypes = this.site.get('post_types');
|
||||
const postType = this.get('whisper') ? postTypes.whisper : postTypes.regular;
|
||||
|
||||
// Build the post object
|
||||
const createdPost = this.store.createRecord('post', {
|
||||
imageSizes: opts.imageSizes,
|
||||
@ -539,9 +584,9 @@ const Composer = RestModel.extend({
|
||||
username: user.get('username'),
|
||||
user_id: user.get('id'),
|
||||
user_title: user.get('title'),
|
||||
uploaded_avatar_id: user.get('uploaded_avatar_id'),
|
||||
avatar_template: user.get('avatar_template'),
|
||||
user_custom_fields: user.get('custom_fields'),
|
||||
post_type: this.site.get('post_types.regular'),
|
||||
post_type: postType,
|
||||
actions_summary: [],
|
||||
moderator: user.get('moderator'),
|
||||
admin: user.get('admin'),
|
||||
@ -559,7 +604,7 @@ const Composer = RestModel.extend({
|
||||
reply_to_post_number: post.get('post_number'),
|
||||
reply_to_user: {
|
||||
username: post.get('username'),
|
||||
uploaded_avatar_id: post.get('uploaded_avatar_id')
|
||||
avatar_template: post.get('avatar_template')
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -103,21 +103,17 @@ NavItem.reopenClass({
|
||||
|
||||
buildList(category, args) {
|
||||
args = args || {};
|
||||
if (category) { args.category = category }
|
||||
|
||||
var items = Discourse.SiteSettings.top_menu.split("|");
|
||||
if (category) { args.category = category; }
|
||||
|
||||
if (args.filterMode && !_.some(items, function(i){
|
||||
return i.indexOf(args.filterMode) !== -1;
|
||||
})) {
|
||||
let items = Discourse.SiteSettings.top_menu.split("|");
|
||||
|
||||
if (args.filterMode && !_.some(items, i => i.indexOf(args.filterMode) !== -1)) {
|
||||
items.push(args.filterMode);
|
||||
}
|
||||
|
||||
return items.map(function(i) {
|
||||
return Discourse.NavItem.fromText(i, args);
|
||||
}).filter(function(i) {
|
||||
return i !== null && !(category && i.get("name").indexOf("categor") === 0);
|
||||
});
|
||||
return items.map(i => Discourse.NavItem.fromText(i, args))
|
||||
.filter(i => i !== null && !(category && i.get("name").indexOf("categor") === 0));
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@ -5,18 +5,13 @@ function calcDayDiff(p1, p2) {
|
||||
if (!p1) { return; }
|
||||
|
||||
const date = p1.get('created_at');
|
||||
if (date) {
|
||||
if (p2) {
|
||||
const numDiff = p1.get('post_number') - p2.get('post_number');
|
||||
if (numDiff === 1) {
|
||||
const lastDate = p2.get('created_at');
|
||||
if (lastDate) {
|
||||
const delta = new Date(date).getTime() - new Date(lastDate).getTime();
|
||||
const days = Math.round(delta / (1000 * 60 * 60 * 24));
|
||||
if (date && p2) {
|
||||
const lastDate = p2.get('created_at');
|
||||
if (lastDate) {
|
||||
const delta = new Date(date).getTime() - new Date(lastDate).getTime();
|
||||
const days = Math.round(delta / (1000 * 60 * 60 * 24));
|
||||
|
||||
p1.set('daysSincePrevious', days);
|
||||
}
|
||||
}
|
||||
p1.set('daysSincePrevious', days);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import RestModel from 'discourse/models/rest';
|
||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
import ActionSummary from 'discourse/models/action-summary';
|
||||
import { url, fmt, propertyEqual } from 'discourse/lib/computed';
|
||||
import { url, propertyEqual } from 'discourse/lib/computed';
|
||||
import Quote from 'discourse/lib/quote';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
@ -77,7 +77,6 @@ const Post = RestModel.extend({
|
||||
|
||||
topicOwner: propertyEqual('topic.details.created_by.id', 'user_id'),
|
||||
hasHistory: Em.computed.gt('version', 1),
|
||||
postElementId: fmt('post_number', 'post_%@'),
|
||||
|
||||
canViewRawEmail: function() {
|
||||
return this.get("user_id") === Discourse.User.currentProp("id") || Discourse.User.currentProp('staff');
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
import Archetype from 'discourse/models/archetype';
|
||||
import PostActionType from 'discourse/models/post-action-type';
|
||||
import Singleton from 'discourse/mixins/singleton';
|
||||
@ -7,30 +8,30 @@ const Site = RestModel.extend({
|
||||
|
||||
isReadOnly: Em.computed.alias('is_readonly'),
|
||||
|
||||
notificationLookup: function() {
|
||||
@computed("notification_types")
|
||||
notificationLookup(notificationTypes) {
|
||||
const result = [];
|
||||
_.each(this.get('notification_types'), function(v,k) {
|
||||
result[v] = k;
|
||||
});
|
||||
_.each(notificationTypes, (v, k) => result[v] = k);
|
||||
return result;
|
||||
}.property('notification_types'),
|
||||
},
|
||||
|
||||
flagTypes: function() {
|
||||
@computed("post_action_types.@each")
|
||||
flagTypes() {
|
||||
const postActionTypes = this.get('post_action_types');
|
||||
if (!postActionTypes) return [];
|
||||
return postActionTypes.filterProperty('is_flag', true);
|
||||
}.property('post_action_types.@each'),
|
||||
},
|
||||
|
||||
topicCountDesc: ['topic_count:desc'],
|
||||
categoriesByCount: Ember.computed.sort('categories', 'topicCountDesc'),
|
||||
|
||||
// Sort subcategories under parents
|
||||
sortedCategories: function() {
|
||||
const cats = this.get('categoriesByCount'),
|
||||
result = [],
|
||||
remaining = {};
|
||||
@computed("categoriesByCount", "categories.@each")
|
||||
sortedCategories(cats) {
|
||||
const result = [],
|
||||
remaining = {};
|
||||
|
||||
cats.forEach(function(c) {
|
||||
cats.forEach(c => {
|
||||
const parentCategoryId = parseInt(c.get('parent_category_id'), 10);
|
||||
if (!parentCategoryId) {
|
||||
result.pushObject(c);
|
||||
@ -40,17 +41,17 @@ const Site = RestModel.extend({
|
||||
}
|
||||
});
|
||||
|
||||
Ember.keys(remaining).forEach(function(parentCategoryId) {
|
||||
Ember.keys(remaining).forEach(parentCategoryId => {
|
||||
const category = result.findBy('id', parseInt(parentCategoryId, 10)),
|
||||
index = result.indexOf(category);
|
||||
index = result.indexOf(category);
|
||||
|
||||
if (index !== -1) {
|
||||
result.replace(index+1, 0, remaining[parentCategoryId]);
|
||||
result.replace(index + 1, 0, remaining[parentCategoryId]);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}.property("categories.@each"),
|
||||
},
|
||||
|
||||
postActionTypeById(id) {
|
||||
return this.get("postActionByIdLookup.action" + id);
|
||||
@ -98,16 +99,14 @@ Site.reopenClass(Singleton, {
|
||||
|
||||
create() {
|
||||
const result = this._super.apply(this, arguments);
|
||||
|
||||
const store = result.store;
|
||||
|
||||
if (result.categories) {
|
||||
result.categoriesById = {};
|
||||
result.categories = _.map(result.categories, function(c) {
|
||||
return result.categoriesById[c.id] = store.createRecord('category', c);
|
||||
});
|
||||
result.categories = _.map(result.categories, c => result.categoriesById[c.id] = store.createRecord('category', c));
|
||||
|
||||
// Associate the categories with their parents
|
||||
result.categories.forEach(function (c) {
|
||||
result.categories.forEach(c => {
|
||||
if (c.get('parent_category_id')) {
|
||||
c.set('parentCategory', result.categoriesById[c.get('parent_category_id')]);
|
||||
}
|
||||
@ -115,16 +114,13 @@ Site.reopenClass(Singleton, {
|
||||
}
|
||||
|
||||
if (result.trust_levels) {
|
||||
result.trustLevels = result.trust_levels.map(function (tl) {
|
||||
return Discourse.TrustLevel.create(tl);
|
||||
});
|
||||
|
||||
result.trustLevels = result.trust_levels.map(tl => Discourse.TrustLevel.create(tl));
|
||||
delete result.trust_levels;
|
||||
}
|
||||
|
||||
if (result.post_action_types) {
|
||||
result.postActionByIdLookup = Em.Object.create();
|
||||
result.post_action_types = _.map(result.post_action_types,function(p) {
|
||||
result.post_action_types = _.map(result.post_action_types, p => {
|
||||
const actionType = PostActionType.create(p);
|
||||
result.postActionByIdLookup.set("action" + p.id, actionType);
|
||||
return actionType;
|
||||
@ -133,7 +129,7 @@ Site.reopenClass(Singleton, {
|
||||
|
||||
if (result.topic_flag_types) {
|
||||
result.topicFlagByIdLookup = Em.Object.create();
|
||||
result.topic_flag_types = _.map(result.topic_flag_types,function(p) {
|
||||
result.topic_flag_types = _.map(result.topic_flag_types, p => {
|
||||
const actionType = PostActionType.create(p);
|
||||
result.topicFlagByIdLookup.set("action" + p.id, actionType);
|
||||
return actionType;
|
||||
@ -141,16 +137,14 @@ Site.reopenClass(Singleton, {
|
||||
}
|
||||
|
||||
if (result.archetypes) {
|
||||
result.archetypes = _.map(result.archetypes,function(a) {
|
||||
result.archetypes = _.map(result.archetypes, a => {
|
||||
a.site = result;
|
||||
return Archetype.create(a);
|
||||
});
|
||||
}
|
||||
|
||||
if (result.user_fields) {
|
||||
result.user_fields = result.user_fields.map(function(uf) {
|
||||
return Ember.Object.create(uf);
|
||||
});
|
||||
result.user_fields = result.user_fields.map(uf => Ember.Object.create(uf));
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@ -174,8 +174,8 @@ const TopicTrackingState = Discourse.Model.extend({
|
||||
if (filter === "new") {
|
||||
list.topics.splice(i, 1);
|
||||
} else {
|
||||
list.topics[i].unseen = false;
|
||||
list.topics[i].dont_sync = true;
|
||||
list.topics[i].set('unseen', false);
|
||||
list.topics[i].set('dont_sync', true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -276,15 +276,17 @@ const TopicTrackingState = Discourse.Model.extend({
|
||||
this.lookupCount("unread", category);
|
||||
}
|
||||
|
||||
let categoryId = category ? Em.get(category, "id") : null;
|
||||
let categoryName = category ? Em.get(category, "name") : null;
|
||||
|
||||
if (name === "new") {
|
||||
return this.countNew(categoryName);
|
||||
return this.countNew(categoryId);
|
||||
} else if (name === "unread") {
|
||||
return this.countUnread(categoryName);
|
||||
return this.countUnread(categoryId);
|
||||
} else {
|
||||
categoryName = name.split("/")[1];
|
||||
if (categoryName) {
|
||||
return this.countCategory(categoryName);
|
||||
return this.countCategory(categoryId);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import RestModel from 'discourse/models/rest';
|
||||
import { url } from 'discourse/lib/computed';
|
||||
import { on } from 'ember-addons/ember-computed-decorators';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
const UserActionTypes = {
|
||||
likes_given: 1,
|
||||
@ -17,21 +19,22 @@ const UserActionTypes = {
|
||||
};
|
||||
const InvertedActionTypes = {};
|
||||
|
||||
_.each(UserActionTypes, function (k, v) {
|
||||
_.each(UserActionTypes, (k, v) => {
|
||||
InvertedActionTypes[k] = v;
|
||||
});
|
||||
|
||||
const UserAction = RestModel.extend({
|
||||
|
||||
_attachCategory: function() {
|
||||
@on("init")
|
||||
_attachCategory() {
|
||||
const categoryId = this.get('category_id');
|
||||
if (categoryId) {
|
||||
this.set('category', Discourse.Category.findById(categoryId));
|
||||
}
|
||||
}.on('init'),
|
||||
},
|
||||
|
||||
descriptionKey: function() {
|
||||
const action = this.get('action_type');
|
||||
@computed("action_type")
|
||||
descriptionKey(action) {
|
||||
if (action === null || Discourse.UserAction.TO_SHOW.indexOf(action) >= 0) {
|
||||
if (this.get('isPM')) {
|
||||
return this.get('sameUser') ? 'sent_by_you' : 'sent_by_user';
|
||||
@ -59,34 +62,39 @@ const UserAction = RestModel.extend({
|
||||
return this.get('targetUser') ? 'user_mentioned_you' : 'user_mentioned_user';
|
||||
}
|
||||
}
|
||||
}.property('action_type'),
|
||||
},
|
||||
|
||||
sameUser: function() {
|
||||
return this.get('username') === Discourse.User.currentProp('username');
|
||||
}.property('username'),
|
||||
@computed("username")
|
||||
sameUser(username) {
|
||||
return username === Discourse.User.currentProp('username');
|
||||
},
|
||||
|
||||
targetUser: function() {
|
||||
return this.get('target_username') === Discourse.User.currentProp('username');
|
||||
}.property('target_username'),
|
||||
@computed("target_username")
|
||||
targetUser(targetUsername) {
|
||||
return targetUsername === Discourse.User.currentProp('username');
|
||||
},
|
||||
|
||||
presentName: Em.computed.any('name', 'username'),
|
||||
targetDisplayName: Em.computed.any('target_name', 'target_username'),
|
||||
actingDisplayName: Em.computed.any('acting_name', 'acting_username'),
|
||||
targetUserUrl: url('target_username', '/users/%@'),
|
||||
|
||||
usernameLower: function() {
|
||||
return this.get('username').toLowerCase();
|
||||
}.property('username'),
|
||||
@computed("username")
|
||||
usernameLower(username) {
|
||||
return username.toLowerCase();
|
||||
},
|
||||
|
||||
userUrl: url('usernameLower', '/users/%@'),
|
||||
|
||||
postUrl: function() {
|
||||
@computed()
|
||||
postUrl() {
|
||||
return Discourse.Utilities.postUrl(this.get('slug'), this.get('topic_id'), this.get('post_number'));
|
||||
}.property(),
|
||||
},
|
||||
|
||||
replyUrl: function() {
|
||||
@computed()
|
||||
replyUrl() {
|
||||
return Discourse.Utilities.postUrl(this.get('slug'), this.get('topic_id'), this.get('reply_to_post_number'));
|
||||
}.property(),
|
||||
},
|
||||
|
||||
replyType: Em.computed.equal('action_type', UserActionTypes.replies),
|
||||
postType: Em.computed.equal('action_type', UserActionTypes.posts),
|
||||
@ -99,7 +107,7 @@ const UserAction = RestModel.extend({
|
||||
postReplyType: Em.computed.or('postType', 'replyType'),
|
||||
removableBookmark: Em.computed.and('bookmarkType', 'sameUser'),
|
||||
|
||||
addChild: function(action) {
|
||||
addChild(action) {
|
||||
let groups = this.get("childGroups");
|
||||
if (!groups) {
|
||||
groups = {
|
||||
@ -143,22 +151,21 @@ const UserAction = RestModel.extend({
|
||||
"childGroups.edits.items", "childGroups.edits.items.@each",
|
||||
"childGroups.bookmarks.items", "childGroups.bookmarks.items.@each"),
|
||||
|
||||
switchToActing: function() {
|
||||
switchToActing() {
|
||||
this.setProperties({
|
||||
username: this.get('acting_username'),
|
||||
uploaded_avatar_id: this.get('acting_uploaded_avatar_id'),
|
||||
name: this.get('actingDisplayName')
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
UserAction.reopenClass({
|
||||
collapseStream: function(stream) {
|
||||
collapseStream(stream) {
|
||||
const uniq = {};
|
||||
const collapsed = [];
|
||||
let pos = 0;
|
||||
|
||||
stream.forEach(function(item) {
|
||||
stream.forEach(item => {
|
||||
const key = "" + item.topic_id + "-" + item.post_number;
|
||||
const found = uniq[key];
|
||||
if (found === void 0) {
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { url } from 'discourse/lib/computed';
|
||||
import RestModel from 'discourse/models/rest';
|
||||
import avatarTemplate from 'discourse/lib/avatar-template';
|
||||
import UserStream from 'discourse/models/user-stream';
|
||||
import UserPostsStream from 'discourse/models/user-posts-stream';
|
||||
import Singleton from 'discourse/mixins/singleton';
|
||||
import { longDate } from 'discourse/lib/formatter';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
import { observes } from 'ember-addons/ember-computed-decorators';
|
||||
import Badge from 'discourse/models/badge';
|
||||
import UserBadge from 'discourse/models/user-badge';
|
||||
|
||||
@ -18,13 +18,15 @@ const User = RestModel.extend({
|
||||
hasNotPosted: Em.computed.not("hasPosted"),
|
||||
canBeDeleted: Em.computed.and("can_be_deleted", "hasNotPosted"),
|
||||
|
||||
stream: function() {
|
||||
@computed()
|
||||
stream() {
|
||||
return UserStream.create({ user: this });
|
||||
}.property(),
|
||||
},
|
||||
|
||||
postsStream: function() {
|
||||
@computed()
|
||||
postsStream() {
|
||||
return UserPostsStream.create({ user: this });
|
||||
}.property(),
|
||||
},
|
||||
|
||||
staff: Em.computed.or('admin', 'moderator'),
|
||||
|
||||
@ -32,27 +34,22 @@ const User = RestModel.extend({
|
||||
return Discourse.ajax(`/session/${this.get('username')}`, { type: 'DELETE'});
|
||||
},
|
||||
|
||||
searchContext: function() {
|
||||
@computed("username_lower")
|
||||
searchContext(username) {
|
||||
return {
|
||||
type: 'user',
|
||||
id: this.get('username_lower'),
|
||||
id: username,
|
||||
user: this
|
||||
};
|
||||
}.property('username_lower'),
|
||||
},
|
||||
|
||||
/**
|
||||
This user's display name. Returns the name if possible, otherwise returns the
|
||||
username.
|
||||
|
||||
@property displayName
|
||||
@type {String}
|
||||
**/
|
||||
displayName: function() {
|
||||
if (Discourse.SiteSettings.enable_names && !Ember.isEmpty(this.get('name'))) {
|
||||
return this.get('name');
|
||||
@computed("username", "name")
|
||||
displayName(username, name) {
|
||||
if (Discourse.SiteSettings.enable_names && !Ember.isEmpty(name)) {
|
||||
return name;
|
||||
}
|
||||
return this.get('username');
|
||||
}.property('username', 'name'),
|
||||
return username;
|
||||
},
|
||||
|
||||
@computed('profile_background')
|
||||
profileBackground(bgUrl) {
|
||||
@ -60,38 +57,23 @@ const User = RestModel.extend({
|
||||
return ('background-image: url(' + Discourse.getURLWithCDN(bgUrl) + ')').htmlSafe();
|
||||
},
|
||||
|
||||
path: function(){
|
||||
return Discourse.getURL('/users/' + this.get('username_lower'));
|
||||
@computed()
|
||||
path() {
|
||||
// no need to observe, requires a hard refresh to update
|
||||
}.property(),
|
||||
return Discourse.getURL(`/users/${this.get('username_lower')}`);
|
||||
},
|
||||
|
||||
/**
|
||||
Path to this user's administration
|
||||
|
||||
@property adminPath
|
||||
@type {String}
|
||||
**/
|
||||
adminPath: url('username_lower', "/admin/users/%@"),
|
||||
|
||||
/**
|
||||
This user's username in lowercase.
|
||||
@computed("username")
|
||||
username_lower(username) {
|
||||
return username.toLowerCase();
|
||||
},
|
||||
|
||||
@property username_lower
|
||||
@type {String}
|
||||
**/
|
||||
username_lower: function() {
|
||||
return this.get('username').toLowerCase();
|
||||
}.property('username'),
|
||||
|
||||
/**
|
||||
This user's trust level.
|
||||
|
||||
@property trustLevel
|
||||
@type {Integer}
|
||||
**/
|
||||
trustLevel: function() {
|
||||
return Discourse.Site.currentProp('trustLevels').findProperty('id', parseInt(this.get('trust_level'), 10));
|
||||
}.property('trust_level'),
|
||||
@computed("trust_level")
|
||||
trustLevel(trustLevel) {
|
||||
return Discourse.Site.currentProp('trustLevels').findProperty('id', parseInt(trustLevel, 10));
|
||||
},
|
||||
|
||||
isBasic: Em.computed.equal('trust_level', 0),
|
||||
isLeader: Em.computed.equal('trust_level', 3),
|
||||
@ -100,61 +82,36 @@ const User = RestModel.extend({
|
||||
|
||||
isSuspended: Em.computed.equal('suspended', true),
|
||||
|
||||
suspended: function() {
|
||||
return this.get('suspended_till') && moment(this.get('suspended_till')).isAfter();
|
||||
}.property('suspended_till'),
|
||||
@computed("suspended_till")
|
||||
suspended(suspendedTill) {
|
||||
return suspendedTill && moment(suspendedTill).isAfter();
|
||||
},
|
||||
|
||||
suspendedTillDate: function() {
|
||||
return longDate(this.get('suspended_till'));
|
||||
}.property('suspended_till'),
|
||||
@computed("suspended_till")
|
||||
suspendedTillDate(suspendedTill) {
|
||||
return longDate(suspendedTill);
|
||||
},
|
||||
|
||||
/**
|
||||
Changes this user's username.
|
||||
|
||||
@method changeUsername
|
||||
@param {String} newUsername The user's new username
|
||||
@returns Result of ajax call
|
||||
**/
|
||||
changeUsername: function(newUsername) {
|
||||
return Discourse.ajax("/users/" + this.get('username_lower') + "/preferences/username", {
|
||||
changeUsername(new_username) {
|
||||
return Discourse.ajax(`/users/${this.get('username_lower')}/preferences/username`, {
|
||||
type: 'PUT',
|
||||
data: { new_username: newUsername }
|
||||
data: { new_username }
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
Changes this user's email address.
|
||||
|
||||
@method changeEmail
|
||||
@param {String} email The user's new email address\
|
||||
@returns Result of ajax call
|
||||
**/
|
||||
changeEmail: function(email) {
|
||||
return Discourse.ajax("/users/" + this.get('username_lower') + "/preferences/email", {
|
||||
changeEmail(email) {
|
||||
return Discourse.ajax(`/users/${this.get('username_lower')}/preferences/email`, {
|
||||
type: 'PUT',
|
||||
data: { email: email }
|
||||
data: { email }
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
Returns a copy of this user.
|
||||
|
||||
@method copy
|
||||
@returns {User}
|
||||
**/
|
||||
copy: function() {
|
||||
copy() {
|
||||
return Discourse.User.create(this.getProperties(Ember.keys(this)));
|
||||
},
|
||||
|
||||
/**
|
||||
Save's this user's properties over AJAX via a PUT request.
|
||||
|
||||
@method save
|
||||
@returns {Promise} the result of the operation
|
||||
**/
|
||||
save: function() {
|
||||
const self = this,
|
||||
data = this.getProperties(
|
||||
save() {
|
||||
const data = this.getProperties(
|
||||
'auto_track_topics_after_msecs',
|
||||
'bio_raw',
|
||||
'website',
|
||||
@ -179,10 +136,10 @@ const User = RestModel.extend({
|
||||
'card_background'
|
||||
);
|
||||
|
||||
['muted','watched','tracked'].forEach(function(s){
|
||||
var cats = self.get(s + 'Categories').map(function(c){ return c.get('id')});
|
||||
['muted','watched','tracked'].forEach(s => {
|
||||
let cats = this.get(s + 'Categories').map(c => c.get('id'));
|
||||
// HACK: denote lack of categories
|
||||
if(cats.length === 0) { cats = [-1]; }
|
||||
if (cats.length === 0) { cats = [-1]; }
|
||||
data[s + '_category_ids'] = cats;
|
||||
});
|
||||
|
||||
@ -192,26 +149,19 @@ const User = RestModel.extend({
|
||||
|
||||
// TODO: We can remove this when migrated fully to rest model.
|
||||
this.set('isSaving', true);
|
||||
return Discourse.ajax("/users/" + this.get('username_lower'), {
|
||||
return Discourse.ajax(`/users/${this.get('username_lower')}`, {
|
||||
data: data,
|
||||
type: 'PUT'
|
||||
}).then(function(result) {
|
||||
self.set('bio_excerpt', result.user.bio_excerpt);
|
||||
|
||||
const userProps = self.getProperties('enable_quoting', 'external_links_in_new_tab', 'dynamic_favicon');
|
||||
}).then(result => {
|
||||
this.set('bio_excerpt', result.user.bio_excerpt);
|
||||
const userProps = this.getProperties('enable_quoting', 'external_links_in_new_tab', 'dynamic_favicon');
|
||||
Discourse.User.current().setProperties(userProps);
|
||||
}).finally(() => {
|
||||
this.set('isSaving', false);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
Changes the password and calls the callback function on AJAX.complete.
|
||||
|
||||
@method changePassword
|
||||
@returns {Promise} the result of the change password operation
|
||||
**/
|
||||
changePassword: function() {
|
||||
changePassword() {
|
||||
return Discourse.ajax("/session/forgot_password", {
|
||||
dataType: 'json',
|
||||
data: { login: this.get('username') },
|
||||
@ -219,73 +169,63 @@ const User = RestModel.extend({
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
Loads a single user action by id.
|
||||
|
||||
@method loadUserAction
|
||||
@param {Integer} id The id of the user action being loaded
|
||||
@returns A stream of the user's actions containing the action of id
|
||||
**/
|
||||
loadUserAction: function(id) {
|
||||
var self = this,
|
||||
stream = this.get('stream');
|
||||
return Discourse.ajax("/user_actions/" + id + ".json", { cache: 'false' }).then(function(result) {
|
||||
loadUserAction(id) {
|
||||
const stream = this.get('stream');
|
||||
return Discourse.ajax(`/user_actions/${id}.json`, { cache: 'false' }).then(result => {
|
||||
if (result && result.user_action) {
|
||||
var ua = result.user_action;
|
||||
const ua = result.user_action;
|
||||
|
||||
if ((self.get('stream.filter') || ua.action_type) !== ua.action_type) return;
|
||||
if (!self.get('stream.filter') && !self.inAllStream(ua)) return;
|
||||
if ((this.get('stream.filter') || ua.action_type) !== ua.action_type) return;
|
||||
if (!this.get('stream.filter') && !this.inAllStream(ua)) return;
|
||||
|
||||
var action = Discourse.UserAction.collapseStream([Discourse.UserAction.create(ua)]);
|
||||
const action = Discourse.UserAction.collapseStream([Discourse.UserAction.create(ua)]);
|
||||
stream.set('itemsLoaded', stream.get('itemsLoaded') + 1);
|
||||
stream.get('content').insertAt(0, action[0]);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
inAllStream: function(ua) {
|
||||
inAllStream(ua) {
|
||||
return ua.action_type === Discourse.UserAction.TYPES.posts ||
|
||||
ua.action_type === Discourse.UserAction.TYPES.topics;
|
||||
},
|
||||
|
||||
// The user's stat count, excluding PMs.
|
||||
statsCountNonPM: function() {
|
||||
var self = this;
|
||||
|
||||
@computed("statsExcludingPms.@each.count")
|
||||
statsCountNonPM() {
|
||||
if (Ember.isEmpty(this.get('statsExcludingPms'))) return 0;
|
||||
var count = 0;
|
||||
_.each(this.get('statsExcludingPms'), function(val) {
|
||||
if (self.inAllStream(val)){
|
||||
let count = 0;
|
||||
_.each(this.get('statsExcludingPms'), val => {
|
||||
if (this.inAllStream(val)) {
|
||||
count += val.count;
|
||||
}
|
||||
});
|
||||
return count;
|
||||
}.property('statsExcludingPms.@each.count'),
|
||||
},
|
||||
|
||||
// The user's stats, excluding PMs.
|
||||
statsExcludingPms: function() {
|
||||
@computed("stats.@each.isPM")
|
||||
statsExcludingPms() {
|
||||
if (Ember.isEmpty(this.get('stats'))) return [];
|
||||
return this.get('stats').rejectProperty('isPM');
|
||||
}.property('stats.@each.isPM'),
|
||||
},
|
||||
|
||||
findDetails: function(options) {
|
||||
var user = this;
|
||||
findDetails(options) {
|
||||
const user = this;
|
||||
|
||||
return PreloadStore.getAndRemove("user_" + user.get('username'), function() {
|
||||
return Discourse.ajax("/users/" + user.get('username') + '.json', {data: options});
|
||||
}).then(function (json) {
|
||||
return PreloadStore.getAndRemove(`user_${user.get('username')}`, () => {
|
||||
return Discourse.ajax(`/users/${user.get('username')}.json`, { data: options });
|
||||
}).then(json => {
|
||||
|
||||
if (!Em.isEmpty(json.user.stats)) {
|
||||
json.user.stats = Discourse.User.groupStats(_.map(json.user.stats,function(s) {
|
||||
json.user.stats = Discourse.User.groupStats(_.map(json.user.stats, s => {
|
||||
if (s.count) s.count = parseInt(s.count, 10);
|
||||
return Discourse.UserActionStat.create(s);
|
||||
}));
|
||||
}
|
||||
|
||||
if (!Em.isEmpty(json.user.custom_groups)) {
|
||||
json.user.custom_groups = json.user.custom_groups.map(function (g) {
|
||||
return Discourse.Group.create(g);
|
||||
});
|
||||
json.user.custom_groups = json.user.custom_groups.map(g => Discourse.Group.create(g));
|
||||
}
|
||||
|
||||
if (json.user.invited_by) {
|
||||
@ -294,12 +234,10 @@ const User = RestModel.extend({
|
||||
|
||||
if (!Em.isEmpty(json.user.featured_user_badge_ids)) {
|
||||
const userBadgesMap = {};
|
||||
UserBadge.createFromJson(json).forEach(function(userBadge) {
|
||||
UserBadge.createFromJson(json).forEach(userBadge => {
|
||||
userBadgesMap[ userBadge.get('id') ] = userBadge;
|
||||
});
|
||||
json.user.featured_user_badges = json.user.featured_user_badge_ids.map(function(id) {
|
||||
return userBadgesMap[id];
|
||||
});
|
||||
json.user.featured_user_badges = json.user.featured_user_badge_ids.map(id => userBadgesMap[id]);
|
||||
}
|
||||
|
||||
if (json.user.card_badge) {
|
||||
@ -311,81 +249,62 @@ const User = RestModel.extend({
|
||||
});
|
||||
},
|
||||
|
||||
findStaffInfo: function() {
|
||||
findStaffInfo() {
|
||||
if (!Discourse.User.currentProp("staff")) { return Ember.RSVP.resolve(null); }
|
||||
var self = this;
|
||||
return Discourse.ajax("/users/" + this.get("username_lower") + "/staff-info.json").then(function(info) {
|
||||
self.setProperties(info);
|
||||
return Discourse.ajax(`/users/${this.get("username_lower")}/staff-info.json`).then(info => {
|
||||
this.setProperties(info);
|
||||
});
|
||||
},
|
||||
|
||||
avatarTemplate: function() {
|
||||
return avatarTemplate(this.get('username'), this.get('uploaded_avatar_id'));
|
||||
}.property('uploaded_avatar_id', 'username'),
|
||||
|
||||
/*
|
||||
Change avatar selection
|
||||
*/
|
||||
pickAvatar: function(uploadId) {
|
||||
var self = this;
|
||||
|
||||
return Discourse.ajax("/users/" + this.get("username_lower") + "/preferences/avatar/pick", {
|
||||
pickAvatar(upload_id, type, avatar_template) {
|
||||
return Discourse.ajax(`/users/${this.get("username_lower")}/preferences/avatar/pick`, {
|
||||
type: 'PUT',
|
||||
data: { upload_id: uploadId }
|
||||
}).then(function(){
|
||||
self.set('uploaded_avatar_id', uploadId);
|
||||
});
|
||||
data: { upload_id, type }
|
||||
}).then(() => this.setProperties({
|
||||
avatar_template,
|
||||
uploaded_avatar_id: upload_id
|
||||
}));
|
||||
},
|
||||
|
||||
/**
|
||||
Determines whether the current user is allowed to upload a file.
|
||||
|
||||
@method isAllowedToUploadAFile
|
||||
@param {String} type The type of the upload (image, attachment)
|
||||
@returns true if the current user is allowed to upload a file
|
||||
**/
|
||||
isAllowedToUploadAFile: function(type) {
|
||||
isAllowedToUploadAFile(type) {
|
||||
return this.get('staff') ||
|
||||
this.get('trust_level') > 0 ||
|
||||
Discourse.SiteSettings['newuser_max_' + type + 's'] > 0;
|
||||
},
|
||||
|
||||
/**
|
||||
Invite a user to the site
|
||||
|
||||
@method createInvite
|
||||
@param {String} email The email address of the user to invite to the site
|
||||
@returns {Promise} the result of the server call
|
||||
**/
|
||||
createInvite: function(email, groupNames) {
|
||||
createInvite(email, group_names) {
|
||||
return Discourse.ajax('/invites', {
|
||||
type: 'POST',
|
||||
data: {email: email, group_names: groupNames}
|
||||
data: { email, group_names }
|
||||
});
|
||||
},
|
||||
|
||||
generateInviteLink: function(email, groupNames, topicId) {
|
||||
generateInviteLink(email, group_names, topic_id) {
|
||||
return Discourse.ajax('/invites/link', {
|
||||
type: 'POST',
|
||||
data: {email: email, group_names: groupNames, topic_id: topicId}
|
||||
data: { email, group_names, topic_id }
|
||||
});
|
||||
},
|
||||
|
||||
updateMutedCategories: function() {
|
||||
@observes("muted_category_ids")
|
||||
updateMutedCategories() {
|
||||
this.set("mutedCategories", Discourse.Category.findByIds(this.muted_category_ids));
|
||||
}.observes("muted_category_ids"),
|
||||
},
|
||||
|
||||
updateTrackedCategories: function() {
|
||||
@observes("tracked_category_ids")
|
||||
updateTrackedCategories() {
|
||||
this.set("trackedCategories", Discourse.Category.findByIds(this.tracked_category_ids));
|
||||
}.observes("tracked_category_ids"),
|
||||
},
|
||||
|
||||
updateWatchedCategories: function() {
|
||||
@observes("watched_category_ids")
|
||||
updateWatchedCategories() {
|
||||
this.set("watchedCategories", Discourse.Category.findByIds(this.watched_category_ids));
|
||||
}.observes("watched_category_ids"),
|
||||
},
|
||||
|
||||
canDeleteAccount: function() {
|
||||
return !Discourse.SiteSettings.enable_sso && this.get('can_delete_account') && ((this.get('reply_count')||0) + (this.get('topic_count')||0)) <= 1;
|
||||
}.property('can_delete_account', 'reply_count', 'topic_count'),
|
||||
@computed("can_delete_account", "reply_count", "topic_count")
|
||||
canDeleteAccount(canDeleteAccount, replyCount, topicCount) {
|
||||
return !Discourse.SiteSettings.enable_sso && canDeleteAccount && ((replyCount || 0) + (topicCount || 0)) <= 1;
|
||||
},
|
||||
|
||||
"delete": function() {
|
||||
if (this.get('can_delete_account')) {
|
||||
@ -398,27 +317,26 @@ const User = RestModel.extend({
|
||||
}
|
||||
},
|
||||
|
||||
dismissBanner: function (bannerKey) {
|
||||
dismissBanner(bannerKey) {
|
||||
this.set("dismissed_banner_key", bannerKey);
|
||||
Discourse.ajax("/users/" + this.get('username'), {
|
||||
Discourse.ajax(`/users/${this.get('username')}`, {
|
||||
type: 'PUT',
|
||||
data: { dismissed_banner_key: bannerKey }
|
||||
});
|
||||
},
|
||||
|
||||
checkEmail: function () {
|
||||
var self = this;
|
||||
return Discourse.ajax("/users/" + this.get("username_lower") + "/emails.json", {
|
||||
checkEmail() {
|
||||
return Discourse.ajax(`/users/${this.get("username_lower")}/emails.json`, {
|
||||
type: "PUT",
|
||||
data: { context: window.location.pathname }
|
||||
}).then(function (result) {
|
||||
}).then(result => {
|
||||
if (result) {
|
||||
self.setProperties({
|
||||
this.setProperties({
|
||||
email: result.email,
|
||||
associated_accounts: result.associated_accounts
|
||||
});
|
||||
}
|
||||
}, function () {});
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
@ -426,14 +344,14 @@ const User = RestModel.extend({
|
||||
User.reopenClass(Singleton, {
|
||||
|
||||
// Find a `Discourse.User` for a given username.
|
||||
findByUsername: function(username, options) {
|
||||
findByUsername(username, options) {
|
||||
const user = User.create({username: username});
|
||||
return user.findDetails(options);
|
||||
},
|
||||
|
||||
// TODO: Use app.register and junk Singleton
|
||||
createCurrent: function() {
|
||||
var userJson = PreloadStore.get('currentUser');
|
||||
createCurrent() {
|
||||
const userJson = PreloadStore.get('currentUser');
|
||||
if (userJson) {
|
||||
const store = Discourse.__container__.lookup('store:main');
|
||||
return store.createRecord('user', userJson);
|
||||
@ -441,56 +359,38 @@ User.reopenClass(Singleton, {
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
Checks if given username is valid for this email address
|
||||
|
||||
@method checkUsername
|
||||
@param {String} username A username to check
|
||||
@param {String} email An email address to check
|
||||
@param {Number} forUserId user id - provide when changing username
|
||||
**/
|
||||
checkUsername: function(username, email, forUserId) {
|
||||
checkUsername(username, email, for_user_id) {
|
||||
return Discourse.ajax('/users/check_username', {
|
||||
data: { username: username, email: email, for_user_id: forUserId }
|
||||
data: { username, email, for_user_id }
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
Groups the user's statistics
|
||||
|
||||
@method groupStats
|
||||
@param {Array} stats Given stats
|
||||
@returns {Object}
|
||||
**/
|
||||
groupStats: function(stats) {
|
||||
var responses = Discourse.UserActionStat.create({
|
||||
groupStats(stats) {
|
||||
const responses = Discourse.UserActionStat.create({
|
||||
count: 0,
|
||||
action_type: Discourse.UserAction.TYPES.replies
|
||||
});
|
||||
|
||||
stats.filterProperty('isResponse').forEach(function (stat) {
|
||||
stats.filterProperty('isResponse').forEach(stat => {
|
||||
responses.set('count', responses.get('count') + stat.get('count'));
|
||||
});
|
||||
|
||||
var result = Em.A();
|
||||
const result = Em.A();
|
||||
result.pushObjects(stats.rejectProperty('isResponse'));
|
||||
|
||||
var insertAt = 0;
|
||||
result.forEach(function(item, index){
|
||||
if(item.action_type === Discourse.UserAction.TYPES.topics || item.action_type === Discourse.UserAction.TYPES.posts){
|
||||
let insertAt = 0;
|
||||
result.forEach((item, index) => {
|
||||
if (item.action_type === Discourse.UserAction.TYPES.topics || item.action_type === Discourse.UserAction.TYPES.posts) {
|
||||
insertAt = index + 1;
|
||||
}
|
||||
});
|
||||
if(responses.count > 0) {
|
||||
if (responses.count > 0) {
|
||||
result.insertAt(insertAt, responses);
|
||||
}
|
||||
return(result);
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
Creates a new account
|
||||
**/
|
||||
createAccount: function(attrs) {
|
||||
createAccount(attrs) {
|
||||
return Discourse.ajax("/users", {
|
||||
data: {
|
||||
name: attrs.accountName,
|
||||
|
||||
@ -34,6 +34,7 @@ export default {
|
||||
app.register('message-bus:main', window.MessageBus, { instantiate: false });
|
||||
injectAll(app, 'messageBus');
|
||||
|
||||
app.register('current-user:main', Discourse.User.current(), { instantiate: false });
|
||||
app.register('topic-tracking-state:main', TopicTrackingState.current(), { instantiate: false });
|
||||
injectAll(app, 'topicTrackingState');
|
||||
|
||||
@ -50,7 +51,6 @@ export default {
|
||||
app.register('session:main', Session.current(), { instantiate: false });
|
||||
injectAll(app, 'session');
|
||||
|
||||
app.register('current-user:main', Discourse.User.current(), { instantiate: false });
|
||||
inject(app, 'currentUser', 'component', 'route', 'controller');
|
||||
|
||||
app.register('location:discourse-location', DiscourseLocation);
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
import { queryParams, filterQueryParams, findTopicList } from 'discourse/routes/build-topic-route';
|
||||
|
||||
// A helper function to create a category route with parameters
|
||||
export default function(filter, params) {
|
||||
export default (filter, params) => {
|
||||
return Discourse.Route.extend({
|
||||
queryParams: queryParams,
|
||||
|
||||
model: function(modelParams) {
|
||||
model(modelParams) {
|
||||
return Discourse.Category.findBySlug(modelParams.slug, modelParams.parentSlug);
|
||||
},
|
||||
|
||||
afterModel: function(model, transition) {
|
||||
afterModel(model, transition) {
|
||||
if (!model) {
|
||||
this.replaceWith('/404');
|
||||
return;
|
||||
@ -20,9 +20,9 @@ export default function(filter, params) {
|
||||
this._retrieveTopicList(model, transition)]);
|
||||
},
|
||||
|
||||
_setupNavigation: function(model) {
|
||||
var noSubcategories = params && !!params.no_subcategories,
|
||||
filterMode = "c/" + Discourse.Category.slugFor(model) + (noSubcategories ? "/none" : "") + "/l/" + filter;
|
||||
_setupNavigation(model) {
|
||||
const noSubcategories = params && !!params.no_subcategories,
|
||||
filterMode = `c/${Discourse.Category.slugFor(model)}${noSubcategories ? "/none" : ""}/l/${filter}`;
|
||||
|
||||
this.controllerFor('navigation/category').setProperties({
|
||||
category: model,
|
||||
@ -32,42 +32,38 @@ export default function(filter, params) {
|
||||
});
|
||||
},
|
||||
|
||||
_createSubcategoryList: function(model) {
|
||||
_createSubcategoryList(model) {
|
||||
this._categoryList = null;
|
||||
if (Em.isNone(model.get('parentCategory')) && Discourse.SiteSettings.show_subcategory_list) {
|
||||
var self = this;
|
||||
return Discourse.CategoryList.listForParent(this.store, model).then(function(list) {
|
||||
self._categoryList = list;
|
||||
});
|
||||
return Discourse.CategoryList.listForParent(this.store, model)
|
||||
.then(list => this._categoryList = list);
|
||||
}
|
||||
|
||||
// If we're not loading a subcategory list just resolve
|
||||
return Em.RSVP.resolve();
|
||||
},
|
||||
|
||||
_retrieveTopicList: function(model, transition) {
|
||||
var listFilter = "c/" + Discourse.Category.slugFor(model) + "/l/" + filter,
|
||||
self = this;
|
||||
_retrieveTopicList(model, transition) {
|
||||
const listFilter = `c/${Discourse.Category.slugFor(model)}/l/${filter}`,
|
||||
findOpts = filterQueryParams(transition.queryParams, params),
|
||||
extras = { cached: this.isPoppedState(transition) };
|
||||
|
||||
var findOpts = filterQueryParams(transition.queryParams, params),
|
||||
extras = { cached: this.isPoppedState(transition) };
|
||||
|
||||
return findTopicList(this.store, this.topicTrackingState, listFilter, findOpts, extras).then(function(list) {
|
||||
return findTopicList(this.store, this.topicTrackingState, listFilter, findOpts, extras).then(list => {
|
||||
Discourse.TopicList.hideUniformCategory(list, model);
|
||||
self.set('topics', list);
|
||||
this.set('topics', list);
|
||||
});
|
||||
},
|
||||
|
||||
titleToken: function() {
|
||||
var filterText = I18n.t('filters.' + filter.replace('/', '.') + '.title', {count: 0}),
|
||||
model = this.currentModel;
|
||||
titleToken() {
|
||||
const filterText = I18n.t('filters.' + filter.replace('/', '.') + '.title', { count: 0 }),
|
||||
model = this.currentModel;
|
||||
|
||||
return I18n.t('filters.with_category', { filter: filterText, category: model.get('name') });
|
||||
},
|
||||
|
||||
setupController: function(controller, model) {
|
||||
var topics = this.get('topics'),
|
||||
periodId = topics.get('for_period') || (filter.indexOf('/') > 0 ? filter.split('/')[1] : '');
|
||||
setupController(controller, model) {
|
||||
const topics = this.get('topics'),
|
||||
periodId = topics.get('for_period') || (filter.indexOf('/') > 0 ? filter.split('/')[1] : '');
|
||||
|
||||
this.controllerFor('navigation/category').set('canCreateTopic', topics.get('can_create_topic'));
|
||||
this.controllerFor('discovery/topics').setProperties({
|
||||
@ -87,7 +83,7 @@ export default function(filter, params) {
|
||||
this.openTopicDraft(topics);
|
||||
},
|
||||
|
||||
renderTemplate: function() {
|
||||
renderTemplate() {
|
||||
this.render('navigation/category', { outlet: 'navigation-bar' });
|
||||
|
||||
if (this._categoryList) {
|
||||
@ -96,15 +92,15 @@ export default function(filter, params) {
|
||||
this.render('discovery/topics', { controller: 'discovery/topics', outlet: 'list-container' });
|
||||
},
|
||||
|
||||
deactivate: function() {
|
||||
deactivate() {
|
||||
this._super();
|
||||
this.searchService.set('searchContext', null);
|
||||
},
|
||||
|
||||
actions: {
|
||||
setNotification: function(notification_level){
|
||||
setNotification(notification_level) {
|
||||
this.currentModel.setNotification(notification_level);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -16,7 +16,7 @@ const DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, {
|
||||
// if default page is categories
|
||||
PreloadStore.remove("topic_list");
|
||||
|
||||
return Discourse.CategoryList.list(this.store, 'categories').then((list) => {
|
||||
return Discourse.CategoryList.list(this.store, 'categories').then(list => {
|
||||
const tracking = this.topicTrackingState;
|
||||
if (tracking) {
|
||||
tracking.sync(list, "categories");
|
||||
@ -34,9 +34,10 @@ const DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, {
|
||||
setupController(controller, model) {
|
||||
controller.set("model", model);
|
||||
|
||||
// Only show either the Create Category or Create Topic button
|
||||
this.controllerFor("navigation/categories").set("canCreateCategory", model.get("can_create_category"));
|
||||
this.controllerFor("navigation/categories").set("canCreateTopic", model.get("can_create_topic") && !model.get("can_create_category"));
|
||||
this.controllerFor("navigation/categories").setProperties({
|
||||
canCreateCategory: model.get("can_create_category"),
|
||||
canCreateTopic: model.get("can_create_topic"),
|
||||
});
|
||||
|
||||
this.openTopicDraft(model);
|
||||
},
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { translateResults, getSearchKey } from "discourse/lib/search";
|
||||
import { translateResults, getSearchKey, isValidSearchTerm } from "discourse/lib/search";
|
||||
|
||||
export default Discourse.Route.extend({
|
||||
queryParams: { q: {}, context_id: {}, context: {} },
|
||||
queryParams: { q: {}, context_id: {}, context: {}, skip_context: {} },
|
||||
|
||||
model(params) {
|
||||
const router = Discourse.__container__.lookup('router:main');
|
||||
@ -11,7 +11,7 @@ export default Discourse.Route.extend({
|
||||
args.search_context = {
|
||||
type: params.context,
|
||||
id: params.context_id
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const searchKey = getSearchKey(args);
|
||||
@ -23,7 +23,7 @@ export default Discourse.Route.extend({
|
||||
}
|
||||
|
||||
return PreloadStore.getAndRemove("search", function() {
|
||||
if (params.q && params.q.length > 2) {
|
||||
if (isValidSearchTerm(params.q)) {
|
||||
return Discourse.ajax("/search", { data: args });
|
||||
} else {
|
||||
return null;
|
||||
|
||||
@ -18,50 +18,52 @@ export default RestrictedUserRoute.extend({
|
||||
showModal('avatar-selector');
|
||||
|
||||
// all the properties needed for displaying the avatar selector modal
|
||||
const controller = this.controllerFor('avatar-selector'),
|
||||
props = this.modelFor('user').getProperties(
|
||||
const props = this.modelFor('user').getProperties(
|
||||
'id',
|
||||
'email',
|
||||
'username',
|
||||
'uploaded_avatar_id',
|
||||
'avatar_template',
|
||||
'system_avatar_template',
|
||||
'gravatar_avatar_template',
|
||||
'custom_avatar_template',
|
||||
'system_avatar_upload_id',
|
||||
'gravatar_avatar_upload_id',
|
||||
'custom_avatar_upload_id'
|
||||
);
|
||||
|
||||
switch (props.uploaded_avatar_id) {
|
||||
case props.system_avatar_upload_id:
|
||||
switch (props.avatar_template) {
|
||||
case props.system_avatar_template:
|
||||
props.selected = "system";
|
||||
break;
|
||||
case props.gravatar_avatar_upload_id:
|
||||
case props.gravatar_avatar_template:
|
||||
props.selected = "gravatar";
|
||||
break;
|
||||
default:
|
||||
props.selected = "uploaded";
|
||||
}
|
||||
|
||||
controller.setProperties(props);
|
||||
this.controllerFor('avatar-selector').setProperties(props);
|
||||
},
|
||||
|
||||
saveAvatarSelection() {
|
||||
const user = this.modelFor('user'),
|
||||
avatarSelector = this.controllerFor('avatar-selector');
|
||||
controller = this.controllerFor('avatar-selector'),
|
||||
selectedUploadId = controller.get("selectedUploadId"),
|
||||
selectedAvatarTemplate = controller.get("selectedAvatarTemplate"),
|
||||
type = controller.get("selected");
|
||||
|
||||
// sends the information to the server if it has changed
|
||||
if (avatarSelector.get('selectedUploadId') !== user.get('uploaded_avatar_id')) {
|
||||
user.pickAvatar(avatarSelector.get('selectedUploadId'))
|
||||
.then(() => {
|
||||
user.setProperties(avatarSelector.getProperties(
|
||||
'system_avatar_upload_id',
|
||||
'gravatar_avatar_upload_id',
|
||||
'custom_avatar_upload_id'
|
||||
));
|
||||
bootbox.alert(I18n.t("user.change_avatar.cache_notice"));
|
||||
});
|
||||
}
|
||||
user.pickAvatar(selectedUploadId, type, selectedAvatarTemplate)
|
||||
.then(() => {
|
||||
user.setProperties(controller.getProperties(
|
||||
'system_avatar_template',
|
||||
'gravatar_avatar_template',
|
||||
'custom_avatar_template'
|
||||
));
|
||||
bootbox.alert(I18n.t("user.change_avatar.cache_notice"));
|
||||
});
|
||||
|
||||
// saves the data back
|
||||
avatarSelector.send('closeModal');
|
||||
controller.send('closeModal');
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
@ -41,10 +41,6 @@ const TopicRoute = Discourse.Route.extend({
|
||||
|
||||
actions: {
|
||||
|
||||
showTopicAdminMenu() {
|
||||
this.controllerFor("topic-admin-menu").send("show");
|
||||
},
|
||||
|
||||
showFlags(model) {
|
||||
showModal('flag', { model });
|
||||
this.controllerFor('flag').setProperties({ selected: null });
|
||||
@ -56,7 +52,7 @@ const TopicRoute = Discourse.Route.extend({
|
||||
},
|
||||
|
||||
showAutoClose() {
|
||||
showModal('edit-topic-auto-close', { model: this.modelFor('topic'), title: 'topic.auto_close_title' });
|
||||
showModal('edit-topic-auto-close', { model: this.modelFor('topic') });
|
||||
this.controllerFor('modal').set('modalClass', 'edit-auto-close-modal');
|
||||
},
|
||||
|
||||
@ -213,7 +209,6 @@ const TopicRoute = Discourse.Route.extend({
|
||||
|
||||
this.controllerFor('header').setProperties({ topic: model, showExtraInfo: false });
|
||||
this.searchService.set('searchContext', model.get('searchContext'));
|
||||
this.controllerFor('topic-admin-menu').set('model', model);
|
||||
|
||||
this.controllerFor('composer').set('topic', model);
|
||||
this.topicTrackingState.trackIncoming('all');
|
||||
|
||||
@ -61,7 +61,7 @@ export default Discourse.Route.extend({
|
||||
|
||||
setupController(controller, user) {
|
||||
controller.set('model', user);
|
||||
this.searchService.set('searchContext', user.get('searchContext'))
|
||||
this.searchService.set('searchContext', user.get('searchContext'));
|
||||
},
|
||||
|
||||
activate() {
|
||||
@ -77,7 +77,7 @@ export default Discourse.Route.extend({
|
||||
this.messageBus.unsubscribe("/users/" + this.modelFor('user').get('username_lower'));
|
||||
|
||||
// Remove the search context
|
||||
this.searchService.set('searchContext', null)
|
||||
this.searchService.set('searchContext', null);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<label>
|
||||
{{input type="checkbox" name="autoCloseBasedOnLastPost" checked=autoCloseBasedOnLastPost}}
|
||||
{{input type="checkbox" checked=autoCloseBasedOnLastPost}}
|
||||
{{i18n 'composer.auto_close.based_on_last_post'}}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
<section class='field'>
|
||||
{{#if category.is_special}}
|
||||
<p class="warning">{{i18n 'category.special_warning'}}</p>
|
||||
{{/if}}
|
||||
<ul class='permission-list'>
|
||||
{{#each category.permissions as |p|}}
|
||||
<li>
|
||||
@ -16,6 +19,8 @@
|
||||
{{view 'select' class="permission-selector" optionValuePath="content.id" optionLabelPath="content.description" content=category.availablePermissions value=selectedPermission}}
|
||||
<button {{action "addPermission" selectedGroup selectedPermission}} class="btn btn-small">{{i18n 'category.add_permission'}}</button>
|
||||
{{else}}
|
||||
<button {{action "editPermissions"}} class="btn btn-small">{{i18n 'category.edit_permissions'}}</button>
|
||||
{{#unless category.is_special}}
|
||||
<button {{action "editPermissions"}} class="btn btn-small">{{i18n 'category.edit_permissions'}}</button>
|
||||
{{/unless}}
|
||||
{{/if}}
|
||||
</section>
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
{{#menu-panel visible=visible}}
|
||||
{{#unless currentUser.read_faq}}
|
||||
{{#if prioritizeFaq}}
|
||||
{{#menu-links}}
|
||||
<li>
|
||||
<li class='heading'>
|
||||
{{#d-link path=faqUrl class="faq-link"}}
|
||||
{{i18n "faq"}}
|
||||
<span class='new'>{{i18n "new_item"}}</span>
|
||||
<span class='badge badge-notification'>{{i18n "new_item"}}</span>
|
||||
{{/d-link}}
|
||||
</li>
|
||||
{{/menu-links}}
|
||||
{{/unless}}
|
||||
{{/if}}
|
||||
|
||||
{{#if currentUser.staff}}
|
||||
{{#menu-links}}
|
||||
@ -32,7 +32,9 @@
|
||||
{{/d-link}}
|
||||
</li>
|
||||
{{/if}}
|
||||
<li>{{d-link route="adminSiteSettings" icon="gear" label="admin.site_settings.title"}}</li>
|
||||
{{#if currentUser.admin}}
|
||||
<li>{{d-link route="adminSiteSettings" icon="gear" label="admin.site_settings.title"}}</li>
|
||||
{{/if}}
|
||||
|
||||
{{plugin-outlet "hamburger-admin"}}
|
||||
{{/menu-links}}
|
||||
@ -81,9 +83,9 @@
|
||||
|
||||
{{#menu-links omitRule="true"}}
|
||||
<li>{{d-link route="about" class="about-link" label="about.simple_title"}}</li>
|
||||
{{#if currentUser.read_faq}}
|
||||
{{#unless prioritizeFaq}}
|
||||
<li>{{d-link path=faqUrl class="faq-link" label="faq"}}</li>
|
||||
{{/if}}
|
||||
{{/unless}}
|
||||
|
||||
{{#if showKeyboardShortcuts}}
|
||||
<li>{{d-link action="keyboardShortcuts" class="keyboard-shortcuts-link" label="keyboard_shortcuts_help.title"}}</li>
|
||||
|
||||
@ -0,0 +1,4 @@
|
||||
<h3>{{i18n title}}</h3>
|
||||
<ul>
|
||||
{{yield}}
|
||||
</ul>
|
||||
@ -23,7 +23,7 @@
|
||||
{{fa-icon 'times'}} {{i18n "bookmarks.remove"}}
|
||||
</button>
|
||||
{{else}}
|
||||
<a href={{grandChild.userUrl}} data-user-card={{grandChild.username}} class='avatar-link'><div class='avatar-wrapper'>{{avatar grandChild imageSize="tiny" extraClasses="actor" ignoreTitle="true"}}</div></a>
|
||||
<a href={{grandChild.userUrl}} data-user-card={{grandChild.username}} class='avatar-link'><div class='avatar-wrapper'>{{avatar grandChild imageSize="tiny" extraClasses="actor" ignoreTitle="true" avatarTemplatePath="acting_avatar_template"}}</div></a>
|
||||
{{#if grandChild.edit_reason}} — <span class="edit-reason">{{grandChild.edit_reason}}</span>{{/if}}
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
|
||||
@ -8,7 +8,6 @@
|
||||
{{/if}}
|
||||
<li class='glyphs'>
|
||||
{{d-link path=bookmarksPath title="user.bookmarks" icon="bookmark"}}
|
||||
{{log siteSettings}}
|
||||
{{#if siteSettings.enable_private_messages}}
|
||||
{{d-link path=messagesPath title="user.private_messages" icon="envelope"}}
|
||||
{{/if}}
|
||||
|
||||
@ -1,8 +1,17 @@
|
||||
{{#if visible}}
|
||||
<div class='contents'>
|
||||
|
||||
{{#if currentUser.staff}}
|
||||
{{#popup-menu visible=optionsVisible hide="hideOptions" title="composer.options"}}
|
||||
<li>
|
||||
{{d-button action="toggleWhisper" icon="eye-slash" label="composer.toggle_whisper"}}
|
||||
</li>
|
||||
{{/popup-menu}}
|
||||
{{/if}}
|
||||
|
||||
{{render "composer-messages"}}
|
||||
<div class='control'>
|
||||
<a href class='toggler' {{action "toggle" bubbles=false}} title='{{i18n 'composer.toggler'}}'></a>
|
||||
<a href class='toggler' {{action "toggle" bubbles=false}} title={{i18n 'composer.toggler'}}></a>
|
||||
|
||||
{{#if model.viewOpen}}
|
||||
<div class='control-row reply-area'>
|
||||
@ -11,6 +20,10 @@
|
||||
|
||||
<div class='reply-to'>
|
||||
{{{model.actionTitle}}}
|
||||
{{#if model.whisper}}
|
||||
<span class='whisper'>({{i18n "composer.whisper"}})</span>
|
||||
{{/if}}
|
||||
|
||||
{{#if canEdit}}
|
||||
{{#if showEditReason}}
|
||||
<div class="edit-reason-input">
|
||||
@ -48,7 +61,7 @@
|
||||
{{popup-input-tip validation=view.titleValidation shownAt=view.showTitleTip}}
|
||||
</div>
|
||||
|
||||
{{#unless model.privateMessage}}
|
||||
{{#if model.showCategoryChooser}}
|
||||
<div class="category-input">
|
||||
{{category-chooser valueAttribute="id" value=model.categoryId scopedCategoryId=scopedCategoryId tabindex="3"}}
|
||||
{{popup-input-tip validation=view.categoryValidation shownAt=view.showCategoryTip}}
|
||||
@ -57,9 +70,10 @@
|
||||
<button class='btn' {{action "showOptions"}}>{{i18n 'topic.options'}}</button>
|
||||
{{/if}}
|
||||
{{render "additional-composer-buttons" model}}
|
||||
{{/unless}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{plugin-outlet "composer-fields"}}
|
||||
</div>
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<div class="search row clearfix">
|
||||
{{input type="text" value=searchTerm class="input-xxlarge search no-blur" action="search"}}
|
||||
{{d-button action="search" icon="search" class="btn-primary"}}
|
||||
{{search-text-field value=searchTerm class="input-xxlarge search no-blur" action="search" hasAutofocus=hasAutofocus}}
|
||||
{{d-button action="search" icon="search" class="btn-primary" disabled=isNotValidSearchTerm}}
|
||||
{{#if canBulkSelect}}
|
||||
{{#if model.posts}}
|
||||
{{d-button icon="list" class="bulk-select" title="topics.bulk.toggle" action="toggleBulkSelect"}}
|
||||
|
||||
@ -1,8 +1,3 @@
|
||||
{{plugin-outlet "header-before-dropdowns"}}
|
||||
{{user-menu visible=userMenuVisible logoutAction="logout"}}
|
||||
{{hamburger-menu visible=hamburgerVisible showKeyboardAction="showKeyboardShortcutsHelp"}}
|
||||
{{search-menu visible=searchVisible}}
|
||||
|
||||
<div class='wrap'>
|
||||
<div class='contents clearfix'>
|
||||
{{home-logo minimized=showExtraInfo}}
|
||||
@ -54,6 +49,10 @@
|
||||
{{/header-dropdown}}
|
||||
{{/if}}
|
||||
</ul>
|
||||
{{plugin-outlet "header-before-dropdowns"}}
|
||||
{{user-menu visible=userMenuVisible logoutAction="logout"}}
|
||||
{{hamburger-menu visible=hamburgerVisible showKeyboardAction="showKeyboardShortcutsHelp"}}
|
||||
{{search-menu visible=searchVisible}}
|
||||
</div>
|
||||
|
||||
{{#if showExtraInfo}}
|
||||
|
||||
@ -0,0 +1,8 @@
|
||||
{{#if view.showBadges}}
|
||||
{{raw "topic-post-badges" unread=topic.unread newPosts=topic.displayNewPosts unseen=topic.unseen url=topic.lastUnreadUrl}}
|
||||
{{else}}
|
||||
{{#if topic.unseen}}
|
||||
<span class="badge-notification new-topic"></span>
|
||||
{{/if}}
|
||||
{{raw "list/posts-count-column" topic=topic tagName="div"}}
|
||||
{{/if}}
|
||||
@ -1,5 +1,5 @@
|
||||
<td class='posters'>
|
||||
{{#each poster in posters}}
|
||||
<a href="{{poster.user.path}}" data-user-card="{{poster.user.username}}" class="{{poster.extras}}">{{avatar poster usernamePath="user.username" imageSize="small"}}</a>
|
||||
<a href="{{poster.user.path}}" data-user-card="{{poster.user.username}}" class="{{poster.extras}}">{{avatar poster avatarTemplatePath="user.avatar_template" usernamePath="user.username" imageSize="small"}}</a>
|
||||
{{/each}}
|
||||
</td>
|
||||
|
||||
@ -5,13 +5,9 @@
|
||||
{{#each t in topics}}
|
||||
<tr {{bind-attr class="t.archived"}}>
|
||||
<td>
|
||||
<div class='main-link clearfix'>
|
||||
<div class='main-link'>
|
||||
{{topic-status topic=t}}
|
||||
{{topic-link t}}
|
||||
{{topic-post-badges unread=t.unread
|
||||
newPosts=t.new_posts
|
||||
unseen=t.unseen
|
||||
url=t.lastUnreadUrl}}
|
||||
|
||||
{{#if t.hasExcerpt}}
|
||||
<div class="topic-excerpt">
|
||||
@ -25,10 +21,14 @@
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class='pull-right'>
|
||||
{{raw "list/post-count-or-badges" topic=t postBadgesEnabled="true"}}
|
||||
</div>
|
||||
<div class='clearfix'></div>
|
||||
<div class="topic-item-stats clearfix">
|
||||
<div class="pull-right">
|
||||
{{raw "list/posts-count-column" topic=t tagName="div"}}
|
||||
{{raw "list/activity-column" topic=t tagName="div" class="num activity last"}}
|
||||
<a href="{{t.lastPostUrl}}" title='{{i18n 'last_post'}}: {{{raw-date t.bumped_at}}}'>{{t.last_poster_username}}</a>
|
||||
</div>
|
||||
{{#unless controller.hideCategory}}
|
||||
<div class='category'>
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
<td>
|
||||
<div class='main-link clearfix'>
|
||||
<div class='main-link'>
|
||||
{{raw "topic-status" topic=content}}
|
||||
{{topic-link content}}
|
||||
{{#if controller.showTopicPostBadges}}
|
||||
{{raw "topic-post-badges" unread=content.unread newPosts=content.displayNewPosts unseen=content.unseen url=content.lastUnreadUrl}}
|
||||
{{/if}}
|
||||
{{raw "list/topic-excerpt" topic=content}}
|
||||
</div>
|
||||
<div class='pull-right'>
|
||||
{{raw "list/post-count-or-badges" topic=content postBadgesEnabled=controller.showTopicPostBadges}}
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<div class="topic-item-stats clearfix">
|
||||
{{#unless controller.hideCategory}}
|
||||
@ -17,10 +18,9 @@
|
||||
{{plugin-outlet "topic-list-tags"}}
|
||||
|
||||
<div class="pull-right">
|
||||
{{raw "list/posts-count-column" topic=content tagName="div"}}
|
||||
<div class='num activity last'>
|
||||
<a href="{{content.lastPostUrl}}" title='{{i18n 'last_post'}}: {{{raw-date content.bumped_at}}}'>{{content.last_poster_username}}</a>
|
||||
{{raw "list/activity-column" topic=content tagName="span" class="age"}}
|
||||
<a href="{{content.lastPostUrl}}" title='{{i18n 'last_post'}}: {{{raw-date content.bumped_at}}}'>{{content.last_poster_username}}</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
|
||||
@ -2,32 +2,27 @@
|
||||
<div>
|
||||
<div>
|
||||
<input type="radio" id="system-avatar" name="avatar" value="system" {{action "useSystem"}}>
|
||||
<label class="radio" for="system-avatar">{{bound-avatar controller "large" system_avatar_upload_id}} {{{i18n 'user.change_avatar.letter_based'}}}</label>
|
||||
<label class="radio" for="system-avatar">{{bound-avatar-template system_avatar_template "large"}} {{{i18n 'user.change_avatar.letter_based'}}}</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="radio" id="gravatar" name="avatar" value="gravatar" {{action "useGravatar"}}>
|
||||
<label class="radio" for="gravatar">{{bound-avatar controller "large" gravatar_avatar_upload_id}} {{{i18n 'user.change_avatar.gravatar'}}} {{email}}</label>
|
||||
<label class="radio" for="gravatar">{{bound-avatar-template gravatar_avatar_template "large"}} {{{i18n 'user.change_avatar.gravatar'}}} {{email}}</label>
|
||||
{{d-button action="refreshGravatar" title="user.change_avatar.refresh_gravatar_title" disabled=gravatarRefreshDisabled icon="refresh"}}
|
||||
</div>
|
||||
{{#if allowImageUpload}}
|
||||
<div>
|
||||
<input type="radio" id="uploaded_avatar" name="avatar" value="uploaded" {{action "useUploadedAvatar"}}>
|
||||
<label class="radio" for="uploaded_avatar">
|
||||
{{#if hasUploadedAvatar}}
|
||||
{{#if uploadedAvatarTemplate}}
|
||||
{{bound-avatar-template uploadedAvatarTemplate "large"}}
|
||||
{{else}}
|
||||
{{bound-avatar controller "large" custom_avatar_upload_id}}
|
||||
{{/if}}
|
||||
{{#if custom_avatar_template}}
|
||||
{{bound-avatar-template custom_avatar_template "large"}}
|
||||
{{i18n 'user.change_avatar.uploaded_avatar'}}
|
||||
{{else}}
|
||||
{{i18n 'user.change_avatar.uploaded_avatar_empty'}}
|
||||
{{/if}}
|
||||
</label>
|
||||
{{avatar-uploader username=username
|
||||
user_id=id
|
||||
uploadedAvatarTemplate=uploadedAvatarTemplate
|
||||
custom_avatar_upload_id=custom_avatar_upload_id
|
||||
{{avatar-uploader user_id=id
|
||||
uploadedAvatarTemplate=custom_avatar_template
|
||||
uploadedAvatarId=custom_avatar_upload_id
|
||||
uploading=uploading
|
||||
done="useUploadedAvatar"}}
|
||||
</div>
|
||||
@ -36,6 +31,6 @@
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
{{d-button action="saveAvatarSelection" class="btn-primary" disabled=saveDisabled label="save"}}
|
||||
{{d-button action="saveAvatarSelection" class="btn-primary" disabled=uploading label="save"}}
|
||||
<a {{action "closeModal"}}>{{i18n 'cancel'}}</a>
|
||||
</div>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<form {{action "saveAutoClose" on="submit"}}>
|
||||
<form>
|
||||
<div class="modal-body">
|
||||
{{auto-close-form autoCloseTime=model.auto_close_time
|
||||
autoCloseValid=auto_close_valid
|
||||
@ -6,8 +6,8 @@
|
||||
limited=model.details.auto_close_based_on_last_post }}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class='btn btn-primary' type='submit' {{bind-attr disabled="auto_close_invalid"}}>{{i18n 'topic.auto_close_save'}}</button>
|
||||
{{d-button class="btn-primary" disabled=auto_close_invalid label="topic.auto_close_save" action="saveAutoClose"}}
|
||||
<a {{action "closeModal"}}>{{i18n 'cancel'}}</a>
|
||||
<button class='btn pull-right' {{action "removeAutoClose"}}>{{i18n 'topic.auto_close_remove'}}</button>
|
||||
{{d-button class="pull-right" action="removeAutoClose" label="topic.auto_close_remove"}}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@ -1,36 +1,33 @@
|
||||
<div>
|
||||
<div class="modal-body reorder-categories">
|
||||
<div id="rc-scroll-anchor"></div>
|
||||
<table>
|
||||
<thead>
|
||||
<th class="th-pos">Position</th>
|
||||
<th class="th-cat">Category</th>
|
||||
</thead>
|
||||
{{#each categoriesGrouped as |group|}}
|
||||
<tbody>
|
||||
{{#each group.cats as |cat|}}
|
||||
<tr data-category-id="{{cat.id}}">
|
||||
<td>
|
||||
{{number-field number=cat.position}}
|
||||
{{d-button class="no-text" action="moveUp" actionParam=cat icon="arrow-up"}}
|
||||
{{d-button class="no-text" action="moveDown" actionParam=cat icon="arrow-down"}}
|
||||
{{#if cat.hasBufferedChanges}}
|
||||
{{d-button class="no-text" action="commit" icon="check"}}
|
||||
{{/if}}
|
||||
</td>
|
||||
<td>{{category-badge cat allowUncategorized="true"}}</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
{{/each}}
|
||||
</table>
|
||||
<div id="rc-scroll-bottom"></div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
{{#if showApplyAll}}
|
||||
{{d-button action="commit" icon="check" label="categories.reorder.apply_all"}}
|
||||
{{/if}}
|
||||
{{d-button class="btn-primary" disabled=saveDisabled action="saveOrder" label="categories.reorder.save"}}
|
||||
</div>
|
||||
<div class="modal-body reorder-categories full-height-modal">
|
||||
<div id="rc-scroll-anchor"></div>
|
||||
<table>
|
||||
<thead>
|
||||
<th class="th-pos">{{i18n "categories.reorder.position"}}</th>
|
||||
<th class="th-cat">{{i18n "categories.category"}}</th>
|
||||
</thead>
|
||||
{{#each categoriesOrdered as |cat|}}
|
||||
<tr data-category-id="{{cat.id}}">
|
||||
<td>
|
||||
{{number-field number=cat.position}}
|
||||
{{d-button class="no-text" action="moveUp" actionParam=cat icon="arrow-up"}}
|
||||
{{d-button class="no-text" action="moveDown" actionParam=cat icon="arrow-down"}}
|
||||
{{#if cat.hasBufferedChanges}}
|
||||
{{d-button class="no-text" action="commit" icon="check"}}
|
||||
{{/if}}
|
||||
</td>
|
||||
<td>{{category-badge cat allowUncategorized="true"}}</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</table>
|
||||
<div id="rc-scroll-bottom"></div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
{{#if showFixIndices}}
|
||||
{{d-button action="fixIndices" icon="random" label="categories.reorder.fix_order" title="categories.reorder.fix_order_tooltip"}}
|
||||
{{/if}}
|
||||
{{#if showApplyAll}}
|
||||
{{d-button action="commit" icon="check" label="categories.reorder.apply_all"}}
|
||||
{{/if}}
|
||||
{{d-button class="btn-primary" disabled=saveDisabled action="saveOrder" label="categories.reorder.save"}}
|
||||
</div>
|
||||
|
||||
@ -3,10 +3,7 @@
|
||||
{{navigation-bar navItems=navItems filterMode=filterMode}}
|
||||
|
||||
{{#if canCreateCategory}}
|
||||
{{d-button action="createCategory" icon="plus" label="category.create"}}
|
||||
{{#if siteSettings.fixed_category_positions}}
|
||||
{{d-button action="reorderCategories" icon="random" label="category.reorder"}}
|
||||
{{/if}}
|
||||
{{categories-admin-dropdown}}
|
||||
{{/if}}
|
||||
{{#if canCreateTopic}}
|
||||
<button id="create-topic" class='btn btn-default' {{action "createTopic"}}><i class='fa fa-plus'></i>{{i18n 'topic.create'}}</button>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user