Version bump

This commit is contained in:
Neil Lalonde 2016-12-07 17:50:59 -05:00
commit 80535b09f1
255 changed files with 2813 additions and 1205 deletions

View File

@ -55,4 +55,4 @@ install:
- bash -c "if [ '$RAILS_MASTER' == '1' ]; then bundle update --retry=3 --jobs=3 arel rails rails-observers seed-fu; fi"
- bash -c "if [ '$RAILS_MASTER' == '0' ]; then bundle install --without development --deployment --retry=3 --jobs=3; fi"
script: 'bundle exec rspec && bundle exec rake plugin:spec && bundle exec rake qunit:test'
script: "bundle exec rspec && bundle exec rake plugin:spec && bundle exec rake qunit:test['200000']"

View File

@ -212,7 +212,7 @@ GEM
omniauth-twitter (1.2.1)
json (~> 1.3)
omniauth-oauth (~> 1.1)
onebox (1.6.0)
onebox (1.6.2)
htmlentities (~> 4.3.4)
moneta (~> 0.8)
multi_json (~> 1.11)

View File

@ -1,7 +1,5 @@
import { popupAjaxError } from 'discourse/lib/ajax-error';
import { propertyEqual } from 'discourse/lib/computed';
import { escapeExpression } from 'discourse/lib/utilities';
import computed from 'ember-addons/ember-computed-decorators';
export default Ember.Controller.extend({
adminGroupsType: Ember.inject.controller(),
@ -37,43 +35,6 @@ export default Ember.Controller.extend({
];
}.property(),
@computed
demoAvatarUrl() {
return Discourse.getURL('/images/avatar.png');
},
@computed('model.flair_url')
flairPreviewIcon() {
return this.get('model.flair_url') && this.get('model.flair_url').substr(0,3) === 'fa-';
},
@computed('flairPreviewIcon')
flairPreviewImage() {
return this.get('model.flair_url') && !this.get('flairPreviewIcon');
},
@computed('flairPreviewImage', 'model.flair_url', 'model.flairBackgroundHexColor', 'model.flairHexColor')
flairPreviewStyle() {
var style = '';
if (this.get('flairPreviewImage')) {
style += 'background-image: url(' + escapeExpression(this.get('model.flair_url')) + '); ';
}
if (this.get('model.flairBackgroundHexColor')) {
style += 'background-color: #' + this.get('model.flairBackgroundHexColor') + ';';
}
if (this.get('model.flairHexColor')) {
style += 'color: #' + this.get('model.flairHexColor') + ';';
}
return style;
},
@computed('model.flairBackgroundHexColor')
flairPreviewClasses() {
if (this.get('model.flairBackgroundHexColor')) {
return 'rounded';
}
},
actions: {
next() {
if (this.get("showingLast")) { return; }

View File

@ -1,6 +1,7 @@
import debounce from 'discourse/lib/debounce';
export default Ember.Controller.extend({
queryParams: ["filter"],
filter: null,
onlyOverridden: false,
filtered: Ember.computed.notEmpty('filter'),

View File

@ -6,12 +6,8 @@ export default Ember.Controller.extend({
fieldTypes: null,
createDisabled: Em.computed.gte('model.length', MAX_FIELDS),
arrangedContent: function() {
return Ember.ArrayProxy.extend(Ember.SortableMixin).create({
sortProperties: ['position'],
content: this.get('model')
});
}.property('model'),
fieldSortOrder: ['position'],
sortedFields: Ember.computed.sort('model', 'fieldSortOrder'),
actions: {
createField() {
@ -20,9 +16,9 @@ export default Ember.Controller.extend({
},
moveUp(f) {
const idx = this.get('arrangedContent').indexOf(f);
const idx = this.get('sortedFields').indexOf(f);
if (idx) {
const prev = this.get('arrangedContent').objectAt(idx-1);
const prev = this.get('sortedFields').objectAt(idx-1);
const prevPos = prev.get('position');
prev.update({ position: f.get('position') });
@ -31,9 +27,9 @@ export default Ember.Controller.extend({
},
moveDown(f) {
const idx = this.get('arrangedContent').indexOf(f);
const idx = this.get('sortedFields').indexOf(f);
if (idx > -1) {
const next = this.get('arrangedContent').objectAt(idx+1);
const next = this.get('sortedFields').objectAt(idx+1);
const nextPos = next.get('position');
next.update({ position: f.get('position') });

View File

@ -1,7 +1,5 @@
export default {
resource: 'admin',
map() {
export default function() {
this.route('admin', { resetNamespace: true }, function() {
this.route('dashboard', { path: '/' });
this.route('adminSiteSettings', { path: '/site_settings', resetNamespace: true }, function() {
this.route('adminSiteSettingsCategory', { path: 'category/:category_id', resetNamespace: true} );
@ -84,5 +82,9 @@ export default {
this.route('adminBadges', { path: '/badges', resetNamespace: true }, function() {
this.route('show', { path: '/:badge_id' });
});
}
this.route('adminPlugins', { path: '/plugins', resetNamespace: true }, function() {
this.route('index', { path: '/' });
});
});
};

View File

@ -4,13 +4,18 @@
{{#if model.automatic}}
<h3>{{model.name}}</h3>
{{else}}
<label for="name">{{i18n 'admin.groups.name'}}</label>
{{text-field name="name" value=model.name placeholderKey="admin.groups.name_placeholder"}}
<label for="name">{{i18n 'group.name'}}</label>
{{text-field name="name" value=model.name placeholderKey="group.name_placeholder"}}
{{/if}}
</div>
{{#if model.id}}
{{#unless model.automatic}}
<div>
<label for="bio">{{i18n 'group.bio'}}</label>
{{d-editor value=model.bio_raw}}
</div>
{{#if model.hasOwners}}
<div>
<label for='owner-list'>{{i18n 'admin.groups.group_owners'}}</label>
@ -101,63 +106,7 @@
{{/unless}}
{{#unless model.automatic}}
<div class="flair_inputs">
<div class="flair_left">
<div>
<label for="flair_url">{{i18n 'admin.groups.flair_url'}}</label>
{{text-field name="flair_url" value=model.flair_url placeholderKey="admin.groups.flair_url_placeholder"}}
</div>
<div>
<label for="flair_bg_color">{{i18n 'admin.groups.flair_bg_color'}}</label>
{{text-field name="flair_bg_color" class="flair_bg_color" value=model.flair_bg_color placeholderKey="admin.groups.flair_bg_color_placeholder"}}
</div>
{{#if flairPreviewIcon}}
<div>
<label for="flair_color">{{i18n 'admin.groups.flair_color'}}</label>
{{text-field name="flair_color" class="flair_color" value=model.flair_color placeholderKey="admin.groups.flair_color_placeholder"}}
</div>
{{/if}}
<br/>
<div>
<strong>{{i18n 'admin.groups.flair_note'}}</strong>
</div>
</div>
{{#if flairPreviewIcon}}
<div class="flair_right">
<div>
<label>{{i18n 'admin.groups.flair_preview'}} Icon</label>
<div class="avatar-flair-preview">
<div class="avatar-wrapper">
<img alt width="45" height="45" src="{{demoAvatarUrl}}" class="avatar actor">
</div>
<div class="avatar-flair demo {{flairPreviewClasses}}" style={{flairPreviewStyle}}>
<i class="fa {{model.flair_url}}"></i>
</div>
</div>
</div>
</div>
{{/if}}
{{#if flairPreviewImage}}
<div class="flair_right">
<div>
<label>{{i18n 'admin.groups.flair_preview'}} Image</label>
<div class="avatar-flair-preview">
<div class="avatar-wrapper">
<img alt width="45" height="45" src="{{demoAvatarUrl}}" class="avatar actor">
</div>
<div class="avatar-flair demo {{flairPreviewClasses}}" style={{flairPreviewStyle}}></div>
</div>
</div>
</div>
{{/if}}
<div class="clearfix"></div>
</div>
{{group-flair-inputs model=model}}
{{/unless}}
<div class='buttons'>

View File

@ -4,11 +4,11 @@
<p class="desc">{{i18n 'admin.user_fields.help'}}</p>
{{#if model}}
{{#each arrangedContent as |uf|}}
{{#each sortedFields as |uf|}}
{{admin-user-field-item userField=uf
fieldTypes=fieldTypes
firstField=arrangedContent.firstObject
lastField=arrangedContent.lastObject
firstField=sortedFields.firstObject
lastField=sortedFields.lastObject
destroyAction="destroy"
moveUpAction="moveUp"
moveDownAction="moveDown"}}

View File

@ -135,7 +135,7 @@ export function buildResolver(baseName) {
},
findPluginTemplate(parsedName) {
var pluginParsedName = this.parseName(parsedName.fullName.replace("template:", "template:javascripts/"));
const pluginParsedName = this.parseName(parsedName.fullName.replace("template:", "template:javascripts/"));
return this.findTemplate(pluginParsedName);
},

View File

@ -0,0 +1,20 @@
import { observes } from 'ember-addons/ember-computed-decorators';
import MountWidget from 'discourse/components/mount-widget';
export default MountWidget.extend({
widget: 'avatar-flair',
@observes('flairURL', 'flairBgColor', 'flairColor')
_rerender() {
this.queueRerender();
},
buildArgs() {
return {
primary_group_flair_url: this.get('flairURL'),
primary_group_flair_bg_color: this.get('flairBgColor'),
primary_group_flair_color: this.get('flairColor'),
primary_group_name: this.get('groupName')
};
}
});

View File

@ -13,7 +13,8 @@ export default Ember.Component.extend({
'composer.canEditTitle:edit-title',
'composer.createdPost:created-post',
'composer.creatingTopic:topic',
'composer.whisper:composing-whisper'],
'composer.whisper:composing-whisper',
'composer.showComposerEditor::topic-featured-link-only'],
@computed('composer.composeState')
composeState(composeState) {
@ -27,7 +28,7 @@ export default Ember.Component.extend({
this.appEvents.trigger("composer:resized");
},
@observes('composeState', 'composer.action')
@observes('composeState', 'composer.action', 'composer.canEditTopicFeaturedLink')
resize() {
Ember.run.scheduleOnce('afterRender', () => {
if (!this.element || this.isDestroying || this.isDestroyed) { return; }
@ -76,6 +77,13 @@ export default Ember.Component.extend({
}
},
@observes('composeState')
disableFullscreen() {
if (this.get('composeState') !== Composer.OPEN && positioningWorkaround.blur) {
positioningWorkaround.blur();
}
},
didInsertElement() {
this._super();
const $replyControl = $('#reply-control');

View File

@ -0,0 +1,17 @@
import computed from "ember-addons/ember-computed-decorators";
import UploadMixin from "discourse/mixins/upload";
export default Em.Component.extend(UploadMixin, {
type: "csv",
tagName: "span",
uploadUrl: "/invites/upload_csv",
@computed("uploading")
uploadButtonText(uploading) {
return uploading ? I18n.t("uploading") : I18n.t("user.invited.bulk_invite.text");
},
uploadDone() {
bootbox.alert(I18n.t("user.invited.bulk_invite.success"));
}
});

View File

@ -28,7 +28,7 @@ export default Ember.Component.extend({
}
}
this.appEvents.trigger('modal:body-shown', this.getProperties('title'));
this.appEvents.trigger('modal:body-shown', this.getProperties('title', 'rawTitle'));
},
_flash(msg) {

View File

@ -19,6 +19,8 @@ export default Ember.Component.extend({
this.appEvents.on('modal:body-shown', data => {
if (data.title) {
this.set('title', I18n.t(data.title));
} else if (data.rawTitle) {
this.set('title', data.rawTitle);
}
});
},

View File

@ -0,0 +1,50 @@
import computed from 'ember-addons/ember-computed-decorators';
import { escapeExpression } from 'discourse/lib/utilities';
export default Ember.Component.extend({
classNames: ['group-flair-inputs'],
@computed
demoAvatarUrl() {
return Discourse.getURL('/images/avatar.png');
},
@computed('model.flair_url')
flairPreviewIcon(flairURL) {
return flairURL && flairURL.substr(0,3) === 'fa-';
},
@computed('model.flair_url', 'flairPreviewIcon')
flairPreviewImage(flairURL, flairPreviewIcon) {
return flairURL && !flairPreviewIcon;
},
@computed('model.flair_url', 'flairPreviewImage', 'model.flairBackgroundHexColor', 'model.flairHexColor')
flairPreviewStyle(flairURL, flairPreviewImage, flairBackgroundHexColor, flairHexColor) {
let style = '';
if (flairPreviewImage) {
style += `background-image: url(${escapeExpression(flairURL)});`;
}
if (flairBackgroundHexColor) {
style += `background-color: #${flairBackgroundHexColor};`;
}
if (flairHexColor) style += `color: #${flairHexColor};`;
return style;
},
@computed('model.flairBackgroundHexColor')
flairPreviewClasses(flairBackgroundHexColor) {
if (flairBackgroundHexColor) return 'rounded';
},
@computed('flairPreviewImage')
flairPreviewLabel(flairPreviewImage) {
const key = flairPreviewImage ? 'image' : 'icon';
return I18n.t(`group.flair_preview_${key}`);
}
});

View File

@ -1,4 +1,5 @@
import { findAll } from 'discourse/models/login-method';
import computed from 'ember-addons/ember-computed-decorators';
export default Ember.Component.extend({
elementId: 'login-buttons',
@ -6,9 +7,10 @@ export default Ember.Component.extend({
hidden: Ember.computed.equal('buttons.length', 0),
buttons: function() {
return findAll(this.siteSettings);
}.property(),
@computed
buttons() {
return findAll(this.siteSettings, this.capabilities, this.site.isMobileDevice);
},
actions: {
externalLogin: function(provider) {

View File

@ -15,6 +15,7 @@ export default Ember.Component.extend({
visible: buffer => buffer && buffer.length > 0,
_isMouseDown: false,
_reselected: false,
_selectionChanged() {
const selection = window.getSelection();
@ -43,9 +44,12 @@ export default Ember.Component.extend({
// on Desktop, shows the button at the beginning of the selection
// on Mobile, shows the button at the end of the selection
const isMobileDevice = this.site.isMobileDevice;
const { isIOS, isAndroid } = this.capabilities;
const { isIOS, isAndroid, isSafari } = this.capabilities;
const showAtEnd = isMobileDevice || isIOS || isAndroid;
// used to work around Safari losing selection
const clone = firstRange.cloneRange();
// create a marker element containing a single invisible character
const markerElement = document.createElement("span");
markerElement.appendChild(document.createTextNode("\ufeff"));
@ -64,6 +68,13 @@ export default Ember.Component.extend({
// remove the marker
markerElement.parentNode.removeChild(markerElement);
// work around Safari that would sometimes lose the selection
if (isSafari) {
this._reselected = true;
selection.removeAllRanges();
selection.addRange(clone);
}
// change the position of the button
Ember.run.scheduleOnce("afterRender", () => {
let top = markerOffset.top;
@ -88,6 +99,7 @@ export default Ember.Component.extend({
$(document).on("mousedown.quote-button", (e) => {
this._isMouseDown = true;
this._reselected = false;
if (!willQuote(e)) {
this.sendAction("deselectText");
}
@ -95,7 +107,7 @@ export default Ember.Component.extend({
this._isMouseDown = false;
onSelectionChanged();
}).on("selectionchange.quote-button", () => {
if (!this._isMouseDown) {
if (!this._isMouseDown && !this._reselected) {
onSelectionChanged();
}
});

View File

@ -1,16 +1,16 @@
import { observes } from 'ember-addons/ember-computed-decorators';
const REGEXP_BLOCKS = /(([^" \t\n\x0B\f\r]+)?(("[^"]+")?))/g;
const REGEXP_BLOCKS = /(([^" \t\n\x0B\f\r]+)?(("[^"]+")?))/g;
const REGEXP_USERNAME_PREFIX = /(user:|@)/ig;
const REGEXP_CATEGORY_PREFIX = /(category:|#)/ig;
const REGEXP_GROUP_PREFIX = /group:/ig;
const REGEXP_BADGE_PREFIX = /badge:/ig;
const REGEXP_TAGS_PREFIX = /tags?:/ig;
const REGEXP_IN_PREFIX = /in:/ig;
const REGEXP_STATUS_PREFIX = /status:/ig;
const REGEXP_POST_COUNT_PREFIX = /posts_count:/ig;
const REGEXP_POST_TIME_PREFIX = /(before|after):/ig;
const REGEXP_USERNAME_PREFIX = /(user:|@)/ig;
const REGEXP_CATEGORY_PREFIX = /(category:|#)/ig;
const REGEXP_GROUP_PREFIX = /group:/ig;
const REGEXP_BADGE_PREFIX = /badge:/ig;
const REGEXP_TAGS_PREFIX = /tags?:/ig;
const REGEXP_IN_PREFIX = /in:/ig;
const REGEXP_STATUS_PREFIX = /status:/ig;
const REGEXP_MIN_POST_COUNT_PREFIX = /min_post_count:/ig;
const REGEXP_POST_TIME_PREFIX = /(before|after):/ig;
const REGEXP_IN_MATCH = /in:(posted|watching|tracking|bookmarks|first|pinned|unpinned)/ig;
const REGEXP_SPECIAL_IN_LIKES_MATCH = /in:likes/ig;
@ -73,7 +73,7 @@ export default Em.Component.extend({
}
},
status: '',
posts_count: '',
min_post_count: '',
time: {
when: 'before',
days: ''
@ -99,7 +99,7 @@ export default Em.Component.extend({
this.setSearchedTermSpecialInValue('searchedTerms.special.in.wiki', REGEXP_SPECIAL_IN_WIKI_MATCH);
this.setSearchedTermValue('searchedTerms.status', REGEXP_STATUS_PREFIX);
this.setSearchedTermValueForPostTime();
this.setSearchedTermValue('searchedTerms.posts_count', REGEXP_POST_COUNT_PREFIX);
this.setSearchedTermValue('searchedTerms.min_post_count', REGEXP_MIN_POST_COUNT_PREFIX);
},
findSearchTerms() {
@ -490,17 +490,17 @@ export default Em.Component.extend({
}
},
@observes('searchedTerms.posts_count')
updateSearchTermForPostsCount() {
const match = this.filterBlocks(REGEXP_POST_COUNT_PREFIX);
const postsCountFilter = this.get('searchedTerms.posts_count');
@observes('searchedTerms.min_post_count')
updateSearchTermForMinPostCount() {
const match = this.filterBlocks(REGEXP_MIN_POST_COUNT_PREFIX);
const postsCountFilter = this.get('searchedTerms.min_post_count');
let searchTerm = this.get('searchTerm') || '';
if (postsCountFilter) {
if (match.length !== 0) {
searchTerm = searchTerm.replace(match[0], `posts_count:${postsCountFilter}`);
searchTerm = searchTerm.replace(match[0], `min_post_count:${postsCountFilter}`);
} else {
searchTerm += ` posts_count:${postsCountFilter}`;
searchTerm += ` min_post_count:${postsCountFilter}`;
}
this.set('searchTerm', searchTerm.trim());

View File

@ -65,7 +65,7 @@ export default Ember.Component.extend({
const prevEvent = this.get('prevEvent');
if (prevEvent) {
this._topicScrolled(prevEvent);
Ember.run.scheduleOnce('afterRender', this, this._topicScrolled, prevEvent);
} else {
Ember.run.scheduleOnce('afterRender', this, this._updateProgressBar);
}

View File

@ -27,8 +27,6 @@ export default Ember.Component.extend(bufferedRender({
}.property('disableActions'),
buildBuffer(buffer) {
const self = this;
const renderIcon = function(name, key, actionable) {
const title = escapeExpression(I18n.t(`topic_statuses.${key}.help`)),
startTag = actionable ? "a href" : "span",
@ -39,8 +37,8 @@ export default Ember.Component.extend(bufferedRender({
buffer.push(`<${startTag} title='${title}' class='topic-status'>${icon}</${endTag}>`);
};
const renderIconIf = function(conditionProp, name, key, actionable) {
if (!self.get(conditionProp)) { return; }
const renderIconIf = (conditionProp, name, key, actionable) => {
if (!this.get(conditionProp)) { return; }
renderIcon(name, key, actionable);
};

View File

@ -12,7 +12,7 @@ export default DiscoveryController.extend({
return Discourse.User.currentProp('staff');
},
@computed("model.categories.@each.featuredTopics.length")
@computed("model.categories.[].featuredTopics.length")
latestTopicOnly() {
return this.get("model.categories").find(c => c.get("featuredTopics.length") > 1) === undefined;
},

View File

@ -0,0 +1,20 @@
import { popupAjaxError } from 'discourse/lib/ajax-error';
export default Ember.Controller.extend({
saving: false,
actions: {
save() {
this.set('saving', true);
this.get('model').save().then(() => {
this.transitionToRoute('group', this.get('model.name'));
this.send('closeModal');
}).catch(error => {
popupAjaxError(error);
}).finally(() => {
this.set('saving', false);
});
}
}
});

View File

@ -1,22 +1,11 @@
import { popupAjaxError } from 'discourse/lib/ajax-error';
import computed from 'ember-addons/ember-computed-decorators';
import Group from 'discourse/models/group';
export default Ember.Controller.extend({
loading: false,
limit: null,
offset: null,
@computed('model.owners.[]')
isOwner(owners) {
if (this.get('currentUser.admin')) {
return true;
}
const currentUserId = this.get('currentUser.id');
if (currentUserId) {
return !!owners.findBy('id', currentUserId);
}
},
isOwner: Ember.computed.alias('model.is_group_owner'),
actions: {
removeMember(user) {

View File

@ -12,7 +12,6 @@ var Tab = Em.Object.extend({
}
});
export default Ember.Controller.extend({
counts: null,
showing: 'members',
@ -24,12 +23,29 @@ export default Ember.Controller.extend({
Tab.create({ name: 'messages', requiresMembership: true })
],
@observes('counts')
countsChanged() {
const counts = this.get('counts');
this.get('tabs').forEach(tab => {
tab.set('count', counts.get(tab.get('name')));
});
@computed('model.is_group_owner', 'model.automatic')
canEditGroup(isGroupOwner, automatic) {
return !automatic && isGroupOwner;
},
@computed('model.name', 'model.title')
groupName(name, title) {
return (title || name).capitalize();
},
@computed('model.name', 'model.flair_url', 'model.flair_bg_color', 'model.flair_color')
avatarFlairAttributes(groupName, flairURL, flairBgColor, flairColor) {
return {
primary_group_flair_url: flairURL,
primary_group_flair_bg_color: flairBgColor,
primary_group_flair_color: flairColor,
primary_group_name: groupName
};
},
@observes('model.user_count')
_setMembersTabCount() {
this.get('tabs')[0].set('count', this.get('model.user_count'));
},
@observes('showing')

View File

@ -21,6 +21,9 @@ export default Ember.Controller.extend(ModalFunctionality, {
if (this.site.mobileView) { this.set("viewMode", "inline"); }
}.on("init"),
previousFeaturedLink: Em.computed.alias('model.featured_link_changes.previous'),
currentFeaturedLink: Em.computed.alias('model.featured_link_changes.current'),
previousTagChanges: customTagArray('model.tags_changes.previous'),
currentTagChanges: customTagArray('model.tags_changes.current'),

View File

@ -157,9 +157,7 @@ export default Ember.Controller.extend(CanCheckEmails, {
// Cook the bio for preview
model.set('name', this.get('newNameInput'));
var options = {};
return model.save(options).then(() => {
return model.save().then(() => {
if (Discourse.User.currentProp('id') === model.get('id')) {
Discourse.User.currentProp('name', model.get('name'));
}

View File

@ -5,8 +5,6 @@ import { popupAjaxError } from 'discourse/lib/ajax-error';
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')
@ -20,12 +18,8 @@ export default Ember.Controller.extend(ModalFunctionality, Ember.Evented, {
return categories.map(c => bufProxy.create({ content: c }));
},
categoriesOrdered: function() {
return SortableArrayProxy.create({
sortProperties: ['content.position'],
content: this.get('categoriesBuffered')
});
}.property('categoriesBuffered'),
categoriesSorting: ['position'],
categoriesOrdered: Ember.computed.sort('categoriesBuffered', 'categoriesSorting'),
showFixIndices: function() {
const cats = this.get('categoriesOrdered');

View File

@ -160,6 +160,14 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
return post => this.postSelected(post);
}.property(),
@computed('model.isPrivateMessage', 'model.category.id')
canEditTopicFeaturedLink(isPrivateMessage, categoryId) {
if (!this.siteSettings.topic_featured_link_enabled || isPrivateMessage) { return false; }
const categoryIds = this.site.get('topic_featured_link_allowed_category_ids');
return categoryIds === undefined || !categoryIds.length || categoryIds.indexOf(categoryId) !== -1;
},
@computed('model.isPrivateMessage')
canEditTags(isPrivateMessage) {
return !isPrivateMessage && this.site.get('can_tag_topics');

View File

@ -23,7 +23,7 @@ export default Ember.Controller.extend({
actions: {
exportUserArchive() {
bootbox.confirm(
I18n.t("user.download_archive_confirm"),
I18n.t("user.download_archive.confirm"),
I18n.t("no_value"),
I18n.t("yes_value"),
function(confirmed) {

View File

@ -18,8 +18,6 @@ export default Ember.Controller.extend({
this.set('searchTerm', '');
},
uploadText: function() { return I18n.t("user.invited.bulk_invite.text"); }.property(),
/**
Observe the search term box with a debouncer and change the results.

View File

@ -0,0 +1,6 @@
import { registerUnbound } from 'discourse-common/lib/helpers';
import renderTopicFeaturedLink from 'discourse/lib/render-topic-featured-link';
export default registerUnbound('topic-featured-link', function(topic, params) {
return new Handlebars.SafeString(renderTopicFeaturedLink(topic, params));
});

View File

@ -8,9 +8,9 @@ function exportEntityByType(type, entity, args) {
export function exportUserArchive() {
return exportEntityByType('user', 'user_archive').then(function() {
bootbox.alert(I18n.t("admin.export_csv.success"));
bootbox.alert(I18n.t("user.download_archive.success"));
}).catch(function() {
bootbox.alert(I18n.t("admin.export_csv.rate_limit_error"));
bootbox.alert(I18n.t("user.download_archive.rate_limit_error"));
});
}

View File

@ -21,10 +21,11 @@ export default function interceptClick(e) {
$currentTarget.data('ember-action') ||
$currentTarget.data('auto-route') ||
$currentTarget.data('share-url') ||
$currentTarget.data('user-card') ||
$currentTarget.hasClass('widget-link') ||
$currentTarget.hasClass('mention') ||
(!$currentTarget.hasClass('d-link') && $currentTarget.hasClass('ember-view')) ||
(!$currentTarget.hasClass('d-link') &&
!$currentTarget.data('user-card') &&
$currentTarget.hasClass('ember-view')) ||
$currentTarget.hasClass('lightbox') ||
href.indexOf("mailto:") === 0 ||
(href.match(/^http[s]?:\/\//i) && !href.match(new RegExp("^https?:\\/\\/" + window.location.hostname, "i")))) {

View File

@ -0,0 +1,46 @@
import { extractDomainFromUrl } from 'discourse/lib/utilities';
import { h } from 'virtual-dom';
const _decorators = [];
export function addFeaturedLinkMetaDecorator(decorator) {
_decorators.push(decorator);
}
function extractLinkMeta(topic) {
const href = topic.featured_link, target = Discourse.SiteSettings.open_topic_featured_link_in_external_window ? '_blank' : '';
if (!href) { return; }
let domain = extractDomainFromUrl(href);
if (!domain) { return; }
// www appears frequently, so we truncate it
if (domain && domain.substr(0, 4) === 'www.') {
domain = domain.substring(4);
}
const meta = { target, href, domain, rel: 'nofollow' };
if (_decorators.length) {
_decorators.forEach(cb => cb(meta));
}
return meta;
}
export default function renderTopicFeaturedLink(topic) {
const meta = extractLinkMeta(topic);
if (meta) {
return `<a class="topic-featured-link" rel="${meta.rel}" target="${meta.target}" href="${meta.href}">${meta.domain}</a>`;
} else {
return '';
}
};
export function topicFeaturedLinkNode(topic) {
const meta = extractLinkMeta(topic);
if (meta) {
return h('a.topic-featured-link', {
attributes: { href: meta.href, rel: meta.rel, target: meta.target }
}, meta.domain);
}
}

View File

@ -7,6 +7,8 @@ function applicable() {
}
let workaroundActive = false;
let composingTopic = false;
export function isWorkaroundActive() {
return workaroundActive;
}
@ -22,27 +24,38 @@ function positioningWorkaround($fixedElement) {
var done = false;
var originalScrollTop = 0;
positioningWorkaround.blur = function(evt) {
if (workaroundActive) {
done = true;
$('#main-outlet').show();
$('header').show();
fixedElement.style.position = '';
fixedElement.style.top = '';
fixedElement.style.height = '';
$(window).scrollTop(originalScrollTop);
if (evt) {
evt.target.removeEventListener('blur', blurred);
}
workaroundActive = false;
}
};
var blurredNow = function(evt) {
if (!done && _.include($(document.activeElement).parents(), fixedElement)) {
// something in focus so skip
return;
}
done = true;
$('#main-outlet').show();
$('header').show();
fixedElement.style.position = '';
fixedElement.style.top = '';
fixedElement.style.height = '';
$(window).scrollTop(originalScrollTop);
if (evt) {
evt.target.removeEventListener('blur', blurred);
if (composingTopic) {
return false;
}
workaroundActive = false;
positioningWorkaround.blur(evt);
};
var blurred = _.debounce(blurredNow, 250);
@ -73,7 +86,20 @@ function positioningWorkaround($fixedElement) {
fixedElement.style.top = '0px';
const height = Math.max(parseInt(window.innerHeight*0.6), 350);
let ratio = 0.6;
let min = 350;
composingTopic = false;
if ($('#reply-control select.category-combobox').length > 0) {
composingTopic = true;
// creating a topic, less height
ratio = 0.54;
min = 300;
}
const height = Math.max(parseInt(window.innerHeight*ratio), min);
fixedElement.style.height = height + "px";
// I used to do this, but it seems like we don't need to with position

View File

@ -234,7 +234,7 @@ export function uploadLocation(url) {
} else {
var protocol = window.location.protocol + '//',
hostname = window.location.hostname,
port = ':' + window.location.port;
port = window.location.port ? ':' + window.location.port : '';
return protocol + hostname + port + url;
}
}

View File

@ -5,11 +5,94 @@ const BareRouter = Ember.Router.extend({
location: Ember.testing ? 'none': 'discourse-location'
});
export function mapRoutes() {
// Ember's router can't be extended. We need to allow plugins to add routes to routes that were defined
// in the core app. This class has the same API as Ember's `Router.map` but saves the results in a tree.
// The tree is applied after all plugins are defined.
class RouteNode {
constructor(name, opts={}, depth=0) {
this.name = name;
this.opts = opts;
this.depth = depth;
this.children = [];
this.childrenByName = {};
this.paths = {};
var Router = BareRouter.extend();
const resources = {};
const paths = {};
if (!opts.path) {
opts.path = name;
}
this.paths[opts.path] = true;
}
route(name, opts, fn) {
if (typeof opts === 'function') {
fn = opts;
opts = {};
} else {
opts = opts || {};
}
const existing = this.childrenByName[name];
if (existing) {
if (opts.path) {
existing.paths[opts.path] = true;
}
existing.extract(fn);
} else {
const node = new RouteNode(name, opts, this.depth+1);
node.extract(fn);
this.childrenByName[name] = node;
this.children.push(node);
}
}
extract(fn) {
if (!fn) { return; }
fn.call(this);
}
mapRoutes(router) {
const children = this.children;
if (this.name === 'root') {
children.forEach(c => c.mapRoutes(router));
} else {
const builder = (children.length === 0) ? undefined : function() {
children.forEach(c => c.mapRoutes(this));
};
router.route(this.name, this.opts, builder);
// We can have multiple paths to the same route
const paths = Object.keys(this.paths);
if (paths.length > 1) {
paths.filter(p => p !== this.opts.path).forEach(path => {
const newOpts = jQuery.extend({}, this.opts, { path });
router.route(this.name, newOpts, builder);
});
}
}
}
findSegment(segments) {
if (segments && segments.length) {
const first = segments.shift();
const node = this.childrenByName[first];
if (node) {
return (segments.length === 0) ? node : node.findSegment(segments);
}
}
}
findPath(path) {
if (path) {
return this.findSegment(path.split('.'));
}
}
}
export function mapRoutes() {
const tree = new RouteNode('root');
const extras = [];
// If a module is defined as `route-map` in discourse or a plugin, its routes
// will be built automatically. You can supply a `resource` property to
@ -20,62 +103,24 @@ export function mapRoutes() {
var module = require(key, null, null, true);
if (!module || !module.default) { throw new Error(key + ' must export a route map.'); }
var mapObj = module.default;
const mapObj = module.default;
if (typeof mapObj === 'function') {
mapObj = { resource: 'root', map: mapObj };
tree.extract(mapObj);
} else {
extras.push(mapObj);
}
if (!resources[mapObj.resource]) { resources[mapObj.resource] = []; }
resources[mapObj.resource].push(mapObj.map);
if (mapObj.path) { paths[mapObj.resource] = mapObj.path; }
}
});
return Router.map(function() {
var router = this;
// Do the root resources first
if (resources.root) {
resources.root.forEach(function(m) {
m.call(router);
});
delete resources.root;
extras.forEach(extra => {
const node = tree.findPath(extra.resource);
if (node) {
node.extract(extra.map);
}
});
// Even if no plugins set it up, we need an `adminPlugins` route
var adminPlugins = 'admin.adminPlugins';
resources[adminPlugins] = resources[adminPlugins] || [Ember.K];
paths[adminPlugins] = paths[adminPlugins] || "/plugins";
var segments = {},
standalone = [];
Object.keys(resources).forEach(function(r) {
var m = /^([^\.]+)\.(.*)$/.exec(r);
if (m) {
segments[m[1]] = m[2];
} else {
standalone.push(r);
}
});
// Apply other resources next. A little hacky but works!
standalone.forEach(function(r) {
router.route(r, {path: paths[r], resetNamespace: true}, function() {
var res = this;
resources[r].forEach(function(m) { m.call(res); });
var s = segments[r];
if (s) {
var full = r + '.' + s;
res.route(s, {path: paths[full], resetNamespace: true}, function() {
var nestedRes = this;
resources[full].forEach(function(m) { m.call(nestedRes); });
});
}
});
});
return BareRouter.extend().map(function() {
tree.mapRoutes(this);
this.route('unknown', {path: '*path'});
});
}

View File

@ -169,6 +169,18 @@ const Category = RestModel.extend({
@computed("id")
isUncategorizedCategory(id) {
return id === Discourse.Site.currentProp("uncategorized_category_id");
},
@computed('custom_fields.topic_featured_link_allowed')
topicFeaturedLinkAllowed: {
get(allowed) {
return allowed === "true";
},
set(value) {
value = value ? "true" : "false";
this.set("custom_fields.topic_featured_link_allowed", value);
return value;
}
}
});

View File

@ -32,13 +32,15 @@ const CLOSED = 'closed',
target_usernames: 'targetUsernames',
typing_duration_msecs: 'typingTime',
composer_open_duration_msecs: 'composerTime',
tags: 'tags'
tags: 'tags',
featured_link: 'featuredLink'
},
_edit_topic_serializer = {
title: 'topic.title',
categoryId: 'topic.category.id',
tags: 'topic.tags'
tags: 'topic.tags',
featuredLink: 'topic.featured_link'
};
const Composer = RestModel.extend({
@ -136,6 +138,14 @@ const Composer = RestModel.extend({
canEditTitle: Em.computed.or('creatingTopic', 'creatingPrivateMessage', 'editingFirstPost'),
canCategorize: Em.computed.and('canEditTitle', 'notCreatingPrivateMessage'),
@computed('canEditTitle', 'creatingPrivateMessage', 'categoryId')
canEditTopicFeaturedLink(canEditTitle, creatingPrivateMessage, categoryId) {
if (!this.siteSettings.topic_featured_link_enabled || !canEditTitle || creatingPrivateMessage) { return false; }
const categoryIds = this.site.get('topic_featured_link_allowed_category_ids');
return categoryIds === undefined || !categoryIds.length || categoryIds.indexOf(categoryId) !== -1;
},
// Determine the appropriate title for this action
actionTitle: function() {
const topic = this.get('topic');
@ -180,6 +190,10 @@ const Composer = RestModel.extend({
}.property('action', 'post', 'topic', 'topic.title'),
@computed('canEditTopicFeaturedLink')
showComposerEditor(canEditTopicFeaturedLink) {
return canEditTopicFeaturedLink ? !this.siteSettings.topic_featured_link_onebox : true;
},
// whether to disable the post button
cantSubmitPost: function() {
@ -269,11 +283,12 @@ const Composer = RestModel.extend({
}
}.property('privateMessage'),
missingReplyCharacters: function() {
const postType = this.get('post.post_type');
if (postType === this.site.get('post_types.small_action')) { return 0; }
return this.get('minimumPostLength') - this.get('replyLength');
}.property('minimumPostLength', 'replyLength'),
@computed('minimumPostLength', 'replyLength', 'canEditTopicFeaturedLink')
missingReplyCharacters(minimumPostLength, replyLength, canEditTopicFeaturedLink) {
if (this.get('post.post_type') === this.site.get('post_types.small_action') ||
canEditTopicFeaturedLink && this.siteSettings.topic_featured_link_onebox) { return 0; }
return minimumPostLength - replyLength;
},
/**
Minimum number of characters for a post body to be valid.
@ -492,6 +507,14 @@ const Composer = RestModel.extend({
save(opts) {
if (!this.get('cantSubmitPost')) {
// change category may result in some effect for topic featured link
if (this.get('canEditTopicFeaturedLink')) {
if (this.siteSettings.topic_featured_link_onebox) { this.set('reply', null); }
} else {
this.set('featuredLink', null);
}
return this.get('editingPost') ? this.editPost(opts) : this.createPost(opts);
}
},
@ -512,7 +535,8 @@ const Composer = RestModel.extend({
stagedPost: false,
typingTime: 0,
composerOpened: null,
composerTotalOpened: 0
composerTotalOpened: 0,
featuredLink: null
});
},

View File

@ -114,18 +114,25 @@ const Group = Discourse.Model.extend({
flair_url: this.get('flair_url'),
flair_bg_color: this.get('flairBackgroundHexColor'),
flair_color: this.get('flairHexColor'),
bio_raw: this.get('bio_raw')
};
},
create() {
var self = this;
return ajax("/admin/groups", { type: "POST", data: this.asJSON() }).then(function(resp) {
return ajax("/admin/groups", { type: "POST", data: { group: this.asJSON() } }).then(function(resp) {
self.set('id', resp.basic_group.id);
});
},
save() {
return ajax("/admin/groups/" + this.get('id'), { type: "PUT", data: this.asJSON() });
const id = this.get('id');
const url = this.get('is_group_owner') ? `/groups/${id}` : `/admin/groups/${id}`;
return ajax(url, {
type: "PUT",
data: { group: this.asJSON() }
});
},
destroy() {
@ -166,10 +173,6 @@ Group.reopenClass({
});
},
findGroupCounts(name) {
return ajax("/groups/" + name + "/counts.json").then(result => Em.Object.create(result.counts));
},
find(name) {
return ajax("/groups/" + name + ".json").then(result => Group.create(result.basic_group));
},

View File

@ -28,11 +28,21 @@ export default RestModel.extend({
baseUrl: url('itemsLoaded', 'user.username_lower', '/user_actions.json?offset=%@&username=%@'),
filterBy(filter) {
this.setProperties({ filter, itemsLoaded: 0, content: [], lastLoadedUrl: null });
filterBy(filter, noContentHelpKey) {
this.setProperties({
filter,
itemsLoaded: 0,
content: [],
noContentHelpKey: noContentHelpKey,
lastLoadedUrl: null
});
return this.findItems();
},
noContent: function() {
return this.get('loaded') && this.get('content').length === 0;
}.property('loaded', 'content.@each'),
remove(userAction) {
// 1) remove the user action from the child groups
this.get("content").forEach(function (ua) {
@ -61,6 +71,9 @@ export default RestModel.extend({
if (this.get('filterParam')) {
findUrl += "&filter=" + this.get('filterParam');
}
if (this.get('noContentHelpKey')) {
findUrl += "&no_results_help_key=" + this.get('noContentHelpKey');
}
// Don't load the same stream twice. We're probably at the end.
const lastLoadedUrl = this.get('lastLoadedUrl');
@ -69,6 +82,9 @@ export default RestModel.extend({
if (this.get('loading')) { return Ember.RSVP.resolve(); }
this.set('loading', true);
return ajax(findUrl, {cache: 'false'}).then( function(result) {
if (result && result.no_results_help) {
self.set('noContentHelp', result.no_results_help);
}
if (result && result.user_actions) {
const copy = Em.A();
result.user_actions.forEach(function(action) {
@ -78,11 +94,11 @@ export default RestModel.extend({
self.get('content').pushObjects(UserAction.collapseStream(copy));
self.setProperties({
loaded: true,
itemsLoaded: self.get('itemsLoaded') + result.user_actions.length
});
}
}).finally(function() {
self.set('loaded', true);
self.set('loading', false);
self.set('lastLoadedUrl', findUrl);
});

View File

@ -1,4 +1,4 @@
/*global Modernizr:true*/
/*global Modernizr:true safari:true*/
// Initializes an object that lets us know about our capabilities.
export default {
@ -20,7 +20,7 @@ export default {
caps.isOpera = !!window.opera || ua.indexOf(' OPR/') >= 0;
caps.isFirefox = typeof InstallTrigger !== 'undefined';
caps.isSafari = Object.prototype.toString.call(window.HTMLElement).indexOf('Constructor') > 0;
caps.isSafari = Object.prototype.toString.call(window.HTMLElement).indexOf('Constructor') > 0 || (function (p) { return p.toString() === "[object SafariRemoteNotification]"; })(!window['safari'] || safari.pushNotification);
caps.isChrome = !!window.chrome && !caps.isOpera;
caps.canPasteImages = caps.isChrome || caps.isFirefox;
}

View File

@ -132,7 +132,7 @@ export default function() {
this.route('showCategory' + filter.capitalize(), {path: '/c/:category/:tag_id/l/' + filter});
this.route('showParentCategory' + filter.capitalize(), {path: '/c/:parent_category/:category/:tag_id/l/' + filter});
});
this.route('show', {path: 'intersection/:tag_id/*additional_tags'});
this.route('intersection', {path: 'intersection/:tag_id/*additional_tags'});
});
this.route('tagGroups', {path: '/tag_groups', resetNamespace: true}, function() {

View File

@ -146,10 +146,9 @@ const ApplicationRoute = Discourse.Route.extend(OpenComposer, {
},
changeBulkTemplate(w) {
const controllerName = w.replace('modal/', ''),
factory = getOwner(this).lookupFactory('controller:' + controllerName);
this.render(w, {into: 'modal/topic-bulk-actions', outlet: 'bulkOutlet', controller: factory ? controllerName : 'topic-bulk-actions'});
const controllerName = w.replace('modal/', '');
const controller = getOwner(this).lookup('controller:' + controllerName);
this.render(w, {into: 'modal/topic-bulk-actions', outlet: 'bulkOutlet', controller: controller ? controllerName : 'topic-bulk-actions'});
},
createNewTopicViaParams(title, body, category_id, category, tags) {

View File

@ -1,4 +1,5 @@
import Group from 'discourse/models/group';
import showModal from 'discourse/lib/show-modal';
export default Discourse.Route.extend({
@ -14,13 +15,14 @@ export default Discourse.Route.extend({
return { name: model.get('name').toLowerCase() };
},
afterModel(model) {
return Group.findGroupCounts(model.get('name')).then(counts => {
this.set('counts', counts);
});
},
setupController(controller, model) {
controller.setProperties({ model, counts: this.get('counts') });
},
actions: {
showGroupEditor() {
showModal('edit-group');
this.controllerFor('edit-group').set('model', this.modelFor('group'));
}
}
});

View File

@ -0,0 +1,9 @@
import TagsShowRoute from 'discourse/routes/tags-show';
export default TagsShowRoute.extend({});
// The tags-intersection route is exactly the same as the tags-show route, but the wildcard at the
// end of the route (*additional_tags) will cause a match when query parameters are present,
// breaking all other tags-show routes. Ember thinks the query params are addition tags and should
// be handled by the intersection logic. Defining tags-intersection as something separate avoids
// that confusion.

View File

@ -2,5 +2,6 @@ import UserActivityStreamRoute from "discourse/routes/user-activity-stream";
import UserAction from "discourse/models/user-action";
export default UserActivityStreamRoute.extend({
userActionType: UserAction.TYPES["bookmarks"]
userActionType: UserAction.TYPES["bookmarks"],
noContentHelpKey: "user_activity.no_bookmarks"
});

View File

@ -2,5 +2,6 @@ import UserActivityStreamRoute from "discourse/routes/user-activity-stream";
import UserAction from "discourse/models/user-action";
export default UserActivityStreamRoute.extend({
userActionType: UserAction.TYPES["likes_given"]
userActionType: UserAction.TYPES["likes_given"],
noContentHelpKey: 'user_activity.no_likes_given'
});

View File

@ -6,7 +6,7 @@ export default Discourse.Route.extend(ViewingActionType, {
},
afterModel() {
return this.modelFor("user").get("stream").filterBy(this.get("userActionType"));
return this.modelFor("user").get("stream").filterBy(this.get("userActionType"), this.get("noContentHelpKey"));
},
renderTemplate() {

View File

@ -33,14 +33,6 @@ export default Discourse.Route.extend({
showInvite() {
showModal("invite", { model: this.currentUser });
this.controllerFor("invite").reset();
},
uploadSuccess(filename) {
bootbox.alert(I18n.t("user.invited.bulk_invite.success", { filename: filename }));
},
uploadError(filename, message) {
bootbox.alert(I18n.t("user.invited.bulk_invite.error", { filename: filename, message: message }));
}
}
});

View File

@ -0,0 +1,7 @@
<label class="btn" disabled={{uploading}}>
{{fa-icon "upload"}}&nbsp;{{uploadButtonText}}
<input disabled={{uploading}} type="file" accept=".csv" style="visibility: hidden; position: absolute;" />
</label>
{{#if uploading}}
<span>{{i18n 'upload_selector.uploading'}} {{uploadProgress}}%</span>
{{/if}}

View File

@ -19,6 +19,17 @@
</label>
</section>
{{#if siteSettings.topic_featured_link_enabled}}
<section class='field'>
<div class="allowed-topic-featured-link-category">
<label class="checkbox-label">
{{input type="checkbox" checked=category.topicFeaturedLinkAllowed}}
{{i18n 'category.topic_featured_link_allowed'}}
</label>
</div>
</section>
{{/if}}
<section class="field">
<label>
{{i18n "category.sort_order"}}

View File

@ -0,0 +1,47 @@
<div class="group-flair-left">
<div>
<label for="flair_url">{{i18n 'group.flair_url'}}</label>
{{text-field name="flair_url"
value=model.flair_url
placeholderKey="group.flair_url_placeholder"}}
</div>
<div>
<label for="flair_bg_color">{{i18n 'group.flair_bg_color'}}</label>
{{text-field name="flair_bg_color"
class="group-flair-bg-color"
value=model.flair_bg_color
placeholderKey="group.flair_bg_color_placeholder"}}
</div>
{{#if flairPreviewIcon}}
<div>
<label for="flair_color">{{i18n 'group.flair_color'}}</label>
{{text-field name="flair_color"
class="group-flair-color"
value=model.flair_color
placeholderKey="group.flair_color_placeholder"}}
</div>
{{/if}}
<div>
<strong>{{i18n 'group.flair_note'}}</strong>
</div>
</div>
<div class="group-flair-right">
<label>{{flairPreviewLabel}}</label>
<div class="avatar-flair-preview">
<div class="avatar-wrapper">
<img width="45" height="45" src="{{demoAvatarUrl}}" class="avatar actor">
</div>
{{#if flairPreviewImage}}
<div class="avatar-flair demo {{flairPreviewClasses}}" style={{flairPreviewStyle}}></div>
{{else}}
<div class="avatar-flair demo {{flairPreviewClasses}}" style={{flairPreviewStyle}}>
<i class="fa {{model.flair_url}}"></i>
</div>
{{/if}}
</div>
</div>

View File

@ -7,3 +7,5 @@
{{/each}}
</div>
{{/load-more}}
{{conditional-loading-spinner condition=loading}}

View File

@ -7,12 +7,12 @@
</span>
<span class="category">{{category-link post.category}}</span>
<div class="group-member-info">
{{#if post.user_long_name}}
<span class="name">{{post.user_long_name}}</span>{{#if post.user_title}}<span class="title">, {{post.user_title}}</span>{{/if}}
{{#if post.user.name}}
<span class="name">{{post.user.name}}</span>{{#if post.user.title}}<span class="title">, {{post.user.title}}</span>{{/if}}
{{/if}}
</div>
</div>
<p class='excerpt'>
{{{unbound post.cooked}}}
{{{unbound post.excerpt}}}
</p>
</div>

View File

@ -12,6 +12,9 @@
<tr>
{{topic-status topic=topic}}
{{topic-link topic}}
{{#if topic.featured_link}}
{{topic-featured-link topic}}
{{/if}}
{{topic-post-badges newPosts=topic.totalUnread unseen=topic.unseen url=topic.lastUnreadUrl}}
</tr>
<tr>

View File

@ -75,9 +75,9 @@
</div>
</div>
<div class="control-group pull-left">
<label class="control-label" for="search-posts-count">{{i18n "search.advanced.post.count.label"}}</label>
<label class="control-label" for="search-min-post-count">{{i18n "search.advanced.post.count.label"}}</label>
<div class="controls">
{{input type="number" value=searchedTerms.posts_count class="input-small" id='search-posts-count'}}
{{input type="number" value=searchedTerms.min_post_count class="input-small" id='search-min-post-count'}}
</div>
</div>
</div>

View File

@ -2,12 +2,16 @@
{{bound-category-link topic.category.parentCategory}}
{{/if}}
{{bound-category-link topic.category hideParent=true}}
{{#if siteSettings.tagging_enabled}}
<div class="list-tags">
{{#each topic.tags as |t|}}
{{discourse-tag t}}
{{/each}}
</div>
{{/if}}
<div class="topic-header-extra">
{{#if siteSettings.tagging_enabled}}
<div class="list-tags">
{{#each topic.tags as |t|}}
{{discourse-tag t}}
{{/each}}
</div>
{{/if}}
{{#if siteSettings.topic_featured_link_enabled}}
{{topic-featured-link topic}}
{{/if}}
</div>
{{plugin-outlet "topic-category"}}

View File

@ -4,7 +4,11 @@
<div class="user-card-avatar">
<a href={{user.path}} {{action "showUser"}} class="card-huge-avatar">{{bound-avatar avatar "huge"}}</a>
{{#if user.primary_group_name}}
{{mount-widget widget="avatar-flair" args=user}}
{{avatar-flair
flairURL=user.primary_group_flair_url
flairBgColor=user.primary_group_flair_bg_color
flairColor=user.primary_group_flair_color
groupName=user.primary_group_name}}
{{/if}}
</div>

View File

@ -80,9 +80,13 @@
{{/if}}
{{render "additional-composer-buttons" model}}
{{/if}}
{{#if model.canEditTopicFeaturedLink}}
<div class="topic-featured-link-input">
{{text-field tabindex="4" type="url" value=model.featuredLink id='topic-featured-link' placeholderKey="composer.topic_featured_link_placeholder"}}
</div>
{{/if}}
</div>
{{/if}}
{{plugin-outlet "composer-fields"}}
</div>

View File

@ -1,45 +1,48 @@
{{#if model}}
{{#if isOwner}}
<div class='clearfix'>
<form id='add-user-to-group' autocomplete="off">
{{user-selector usernames=usernames placeholderKey="groups.selector_placeholder" id="user-search-selector" name="usernames"}}
{{d-button action="addMembers" class="add" icon="plus" label="groups.add"}}
</form>
</div>
<form id='add-user-to-group' autocomplete="off">
{{user-selector usernames=usernames placeholderKey="groups.selector_placeholder" id="user-search-selector" name="usernames"}}
{{d-button action="addMembers" class="add" icon="plus" label="groups.add"}}
</form>
{{/if}}
{{#load-more selector=".group-members tr" action="loadMore"}}
<table class='group-members'>
<tr>
<th colspan="2">{{i18n 'last_post'}}</th>
<th>{{i18n 'last_seen'}}</th>
{{#if isOwner}}
<thead>
<th></th>
{{/if}}
</tr>
{{#each model.members as |m|}}
<tr>
<td class='avatar'>
{{user-info user=m}}
{{#if m.owner}}<span class='is-owner'>{{i18n "groups.owner"}}</span>{{/if}}
</td>
<td>
<span class="text">{{bound-date m.last_posted_at}}</span>
</td>
<td>
<span class="text">{{bound-date m.last_seen_at}}</span>
</td>
{{#if isOwner}}
<td class='remove-user'>
{{#unless m.owner}}
<a class="remove-link" {{action "removeMember" m}}><i class="fa fa-times"></i></a>
{{/unless}}
<th>{{i18n 'last_post'}}</th>
<th>{{i18n 'last_seen'}}</th>
<th></th>
</thead>
<tbody>
{{#each model.members as |m|}}
<tr>
<td class='avatar'>
{{#user-info user=m skipName=skipName}}
{{#if m.owner}}<strong class="group-owner-label">{{i18n "groups.owner"}}</strong>{{/if}}
{{/user-info}}
</td>
{{/if}}
</tr>
{{/each}}
<td>
<span class="text">{{bound-date m.last_posted_at}}</span>
</td>
<td>
<span class="text">{{bound-date m.last_seen_at}}</span>
</td>
<td class='remove-user'>
{{#if isOwner}}
{{#unless m.owner}}
<a class="remove-link" {{action "removeMember" m}}><i class="fa fa-times"></i></a>
{{/unless}}
{{/if}}
</td>
</tr>
{{/each}}
</tbody>
</table>
{{/load-more}}
{{conditional-loading-spinner condition=loading}}
{{else}}
<div>{{i18n "groups.empty.users"}}</div>
{{/if}}

View File

@ -1 +1 @@
{{group-post-stream posts=model emptyText=emptyText loadMore="loadMore"}}
{{group-post-stream posts=model emptyText=emptyText loadMore="loadMore" loading=loading}}

View File

@ -1,27 +1,56 @@
<div class="container user-table">
<div class="wrapper">
<section class='user-navigation'>
<ul class='action-list nav-stacked'>
{{#each getTabs as |tab|}}
<li class="{{if tab.active 'active'}}">
{{#link-to tab.location model title=tab.message}}
{{tab.message}}
{{#if tab.count}}<span class='count'>({{tab.count}})</span>{{/if}}
{{/link-to}}
</li>
{{/each}}
</ul>
</section>
<div class="container group">
<div class='group-details-container'>
<div class='group-details'>
{{#if model.flair_url}}
<span class='group-avatar-flair'>
{{avatar-flair
flairURL=model.flair_url
flairBgColor=model.flair_bg_color
flairColor=model.flair_color
groupName=model.name}}
</span>
{{/if}}
<section class='user-main'>
<section class='user-right groups'>
<section class='about group'>
<div class='details'>
<h1>{{model.name}}</h1>
</div>
</section>
<span>
<h1 class='group-header'>
<span class='group-title'>{{groupName}}</span>
</h1>
{{#if model.title}}
<h3 class='group-name'>@{{model.name}}</h3>
{{/if}}
</span>
{{#if canEditGroup}}
<span class="group-edit">
{{d-button action="showGroupEditor" label="group.edit.title" class="group-edit-btn" icon="pencil"}}
</span>
{{/if}}
</div>
{{#if model.bio_cooked}}
<hr/>
<div class='group-bio'>
<p>{{{model.bio_cooked}}}</p>
</div>
{{/if}}
</div>
{{#mobile-nav class='group-nav' desktopClass="pull-left nav nav-stacked" currentPath=currentPath}}
{{#each getTabs as |tab|}}
<li class="{{if tab.active 'active'}}">
{{#link-to tab.location model title=tab.message}}
{{tab.message}}
{{#if tab.count}}<span class='count'>({{tab.count}})</span>{{/if}}
{{/link-to}}
</li>
{{/each}}
{{/mobile-nav}}
<div class='pull-left group-outlet'>
<div class='user-main'>
{{outlet}}
</section>
</section>
</div>
</div>
</div>

View File

@ -7,6 +7,9 @@
<td class='main-link clearfix' colspan="{{titleColSpan}}">
{{raw "topic-status" topic=topic}}
{{topic-link topic}}
{{#if topic.featured_link}}
{{topic-featured-link topic}}
{{/if}}
{{plugin-outlet "topic-list-after-title"}}
{{#if showTopicPostBadges}}
{{raw "topic-post-badges" unread=topic.unread newPosts=topic.displayNewPosts unseen=topic.unseen url=topic.lastUnreadUrl}}

View File

@ -0,0 +1,16 @@
{{#d-modal-body title="group.edit.title" class="edit-group groups"}}
<form class="form-horizontal">
<label for='title'>{{i18n 'group.title'}}</label>
{{input type='text' name='title' value=model.title class='edit-group-title'}}
<label for='bio'>{{i18n 'group.bio'}}</label>
{{d-editor value=model.bio_raw class="edit-group-bio"}}
{{group-flair-inputs model=model}}
</form>
{{/d-modal-body}}
<div class="modal-footer">
{{d-button action="save" class="btn-primary" disabled=saving label="save"}}
<a {{action "closeModal"}}>{{i18n 'cancel'}}</a>
</div>

View File

@ -86,6 +86,13 @@
{{/each}}
</div>
{{/if}}
{{#if model.featured_link_changes}}
<div class='row'>
{{model.featured_link_changes.previous}}
&rarr;
{{model.featured_link_changes.current}}
</div>
{{/if}}
{{plugin-outlet "post-revisions"}}

View File

@ -25,6 +25,9 @@
{{category-chooser valueAttribute="id" value=buffered.category_id}}
{{/if}}
{{#if canEditTopicFeaturedLink}}
{{text-field type="url" value=buffered.featured_link id='topic-featured-link' placeholderKey="composer.topic_featured_link_placeholder"}}
{{/if}}
{{#if canEditTags}}
<br>
{{tag-chooser tags=buffered.tags categoryId=buffered.category_id}}

View File

@ -16,7 +16,7 @@
<div class="pull-right">
{{d-button icon="plus" action="showInvite" label="user.invited.create" class="btn"}}
{{#if canBulkInvite}}
{{resumable-upload target="/invites/upload" success="uploadSuccess" error="uploadError" uploadText=uploadText}}
{{csv-uploader uploading=uploading}}
{{/if}}
{{#if showReinviteAllButton}}
{{#if reinvitedAll}}

View File

@ -23,12 +23,12 @@
{{/link-to}}
</li>
{{/if}}
{{plugin-outlet "user-activity-bottom"}}
{{plugin-outlet "user-activity-bottom" tagName='li'}}
{{/mobile-nav}}
{{#if viewingSelf}}
<div class='user-archive'>
{{d-button action="exportUserArchive" label="user.download_archive" icon="download"}}
{{d-button action="exportUserArchive" label="user.download_archive.button_text" icon="download"}}
</div>
{{/if}}
{{/d-section}}

View File

@ -1,3 +1,8 @@
{{#if model.noContent}}
<div class='no-content'>
{{{model.noContentHelp}}}
</div>
{{/if}}
{{#user-stream stream=model}}
{{#each model.content as |item|}}
{{stream-item item=item removeBookmark="removeBookmark"}}

View File

@ -37,6 +37,7 @@
<li>
{{user-stat value=model.likes_received label="user.summary.likes_received"}}
</li>
{{plugin-outlet "user-summary-stat" tagName="li"}}
</ul>
</div>

View File

@ -37,4 +37,4 @@ createWidget('avatar-flair', {
return [];
}
}
});
});

View File

@ -4,6 +4,7 @@ import { iconNode } from 'discourse/helpers/fa-icon-node';
import DiscourseURL from 'discourse/lib/url';
import RawHtml from 'discourse/widgets/raw-html';
import { tagNode } from 'discourse/lib/render-tag';
import { topicFeaturedLinkNode } from 'discourse/lib/render-topic-featured-link';
export default createWidget('header-topic-info', {
tagName: 'div.extra-info-wrapper',
@ -44,12 +45,19 @@ export default createWidget('header-topic-info', {
title.push(this.attach('category-link', { category }));
}
const extra = [];
if (this.siteSettings.tagging_enabled) {
const tags = topic.get('tags') || [];
if (tags.length) {
title.push(h('div.list-tags', tags.map(tagNode)));
extra.push(h('div.list-tags', tags.map(tagNode)));
}
}
if (this.siteSettings.topic_featured_link_enabled) {
extra.push(topicFeaturedLinkNode(attrs.topic));
}
if (extra) {
title.push(h('div.topic-header-extra', extra));
}
}
const contents = h('div.title-wrapper', title);

View File

@ -5,7 +5,6 @@ import { WidgetClickHook,
WidgetDragHook } from 'discourse/widgets/hooks';
import { h } from 'virtual-dom';
import DecoratorHelper from 'discourse/widgets/decorator-helper';
import { TARGET_NAME } from 'discourse/mixins/delegated-actions';
function emptyContent() { }
@ -272,7 +271,7 @@ export default class Widget {
if (target) {
// TODO: Use ember closure actions
const actions = target[TARGET_NAME] || target.actionHooks || {};
const actions = target.actions || target.actionHooks || {};
const method = actions[actionName];
if (method) {
promise = method.call(target, param);

View File

@ -696,38 +696,6 @@ section.details {
width: 100%;
border-color: dark-light-choose(scale-color($primary, $lightness: 75%), scale-color($secondary, $lightness: 25%));
}
.avatar-flair-preview {
position: relative;
width: 45px;
.avatar-wrapper {
background-color: #f4f4f4;
}
}
.form-horizontal {
.flair_inputs {
margin-top: 30px;
margin-bottom: 30px;
.flair_left {
float: left;
width: 60%;
input[name=flair_url] {
width: 90%;
}
}
.flair_right {
float: left;
margin-left: 30px;
}
}
}
}
.row.groups {
input[type='text'].flair_bg_color, input[type='text'].flair_color {
width: 200px;
}
}
// Customise area

View File

@ -88,6 +88,10 @@ html.anon .topic-list a.title:visited:not(.badge-notification) {color: dark-ligh
}
}
.topic-featured-link {
padding-left: 5px;
}
.topic-excerpt {
font-size: 0.929em;
margin-top: 8px;

View File

@ -187,6 +187,10 @@ div.ac-wrap {
}
}
#reply-control.topic-featured-link-only.open {
.wmd-controls { display: none; }
}
#cancel-file-upload {
font-size: 1.6em;
}

View File

@ -0,0 +1,121 @@
.group-header {
font-size: 2.142em;
font-weight: normal;
}
.group-name {
font-weight: normal;
margin-top: 5px;
color: dark-light-diff($primary, $secondary, 50%, -50%);
}
.group-details-container {
background: rgba(230, 230, 230, 0.3);
padding: 20px;
margin-bottom: 30px;
}
table.group-members {
width: 100%;
th, tr {
border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);
}
th {
text-align: left;
}
tr {
.user-info {
display: block;
}
td {
color: dark-light-diff($primary, $secondary, 50%, -50%);
padding: 0.8em 0;
}
}
}
.group-owner-label {
color: $primary;
}
.group-details {
width: 100%;
}
.group-details {
span {
display: inline-block;
vertical-align: middle;
}
.avatar-flair {
$size: 50px;
background-size: $size;
height: $size;
width: $size;
i {
font-size: $size !important;
}
}
}
.group-edit {
float: right;
}
.groups.edit-group .form-horizontal {
textarea {
width: 99%;
}
label {
font-weight: bold;
}
input[type="text"] {
width: 80% !important;
margin-bottom: 10px;
}
.group-flair-inputs {
display: inline-block;
margin-top: 30px;
margin-bottom: 30px;
.group-flair-left {
float: left;
}
.group-flair-right {
float: left;
margin-left: 30px;
}
}
.avatar-flair-preview {
position: relative;
width: 45px;
.avatar-wrapper {
background-color: #f4f4f4;
}
}
}
#add-user-to-group {
margin: 0px;
.ac-wrap {
width: 100% !important;
}
.add {
margin-top: 10px;
}
}

View File

@ -30,6 +30,7 @@ $input-width: 220px;
.disclaimer {
font-size: 0.9em;
color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%));
clear: both;
}
.user-field.confirm {

View File

@ -27,18 +27,11 @@
}
}
.extra-info-wrapper {
.list-tags {
padding-top: 5px;
}
.discourse-tag {
-webkit-animation: fadein .7s;
animation: fadein .7s;
}
.topic-header-extra .discourse-tag {
-webkit-animation: fadein .7s;
animation: fadein .7s;
}
.add-tags .select2 {
margin: 0;
}
@ -136,11 +129,11 @@ $tag-color: scale-color($primary, $lightness: 40%);
top: -0.1em;
}
header .discourse-tag {color: $tag-color !important; }
header .discourse-tag {color: $tag-color }
.list-tags {
margin-right: 3px;
display: inline;
margin: 0 0 0 5px;
font-size: 0.857em;
}
@ -171,24 +164,6 @@ header .discourse-tag {color: $tag-color !important; }
left: auto;
}
.bullet + .list-tags {
display: block;
line-height: 15px;
}
.bar + .list-tags {
line-height: 1.25;
.discourse-tag {
vertical-align: middle;
}
}
.box + .list-tags {
display: inline-block;
margin: 5px 0 0 5px;
padding-top: 2px;
}
.tag-sort-options {
margin-bottom: 20px;
a {

View File

@ -9,6 +9,10 @@
.badge-wrapper {
float: left;
}
a.topic-featured-link {
display: inline-block;
}
}
a.badge-category {
@ -47,7 +51,7 @@
display: inline;
}
#suggested-topics h3 .badge-wrapper.bullet span.badge-category, {
#suggested-topics h3 .badge-wrapper.bullet span.badge-category {
// Override vertical-align: text-top from `badges.css.scss`
vertical-align: baseline;
line-height: 1.2;
@ -133,3 +137,18 @@
}
}
}
a.topic-featured-link {
display: inline-block;
text-transform: lowercase;
color: #858585;
font-size: 0.875rem;
&::before {
position: relative;
top: 0.1em;
padding-right: 3px;
font-family: FontAwesome;
content: "\f08e";
}
}

View File

@ -133,6 +133,10 @@
}
}
.extra-info-wrapper .title-wrapper .badge-wrapper.bar {
margin-top: 6px;
}
.autocomplete, td.category {
.badge-wrapper {
max-width: 230px;

View File

@ -19,6 +19,7 @@
@import "desktop/history";
@import "desktop/queued-posts";
@import "desktop/menu-panel";
@import "desktop/group";
/* These files doesn't actually exist, they are injected by DiscourseSassImporter. */

View File

@ -151,6 +151,10 @@
font-size: 0.75em;
}
.topic-featured-link {
padding-left: 8px;
}
.topic-list {
.posts {
width: 100%;

View File

@ -298,6 +298,11 @@
background-color: dark-light-diff($primary, $secondary, 90%, -60%);
}
}
#topic-featured-link {
padding: 7px 10px;
margin: 6px 10px 3px 0;
width: 400px;
}
.d-editor-input:disabled {
background-color: dark-light-diff($primary, $secondary, 90%, -60%);
}
@ -465,6 +470,10 @@
}
}
#reply-control.topic-featured-link-only.open {
height: 200px;
}
.control-row.reply-area {
padding-left: 20px;
padding-right: 20px;

View File

@ -0,0 +1,12 @@
.group-outlet {
width: 75%;
}
.group-nav {
width: 20%;
margin-right: 30px;
}
.group-details {
margin-bottom: 20px;
}

View File

@ -505,13 +505,13 @@ video {
.extra-info-wrapper {
overflow: hidden;
.badge-wrapper, i, .topic-link {
.badge-wrapper, i, .topic-link {
-webkit-animation: fadein .7s;
animation: fadein .7s;
}
.topic-statuses {
i { color: $header_primary; }
i { color: $header_primary; }
i.fa-envelope { color: $danger; }
.unpinned { color: $header_primary; }
}
@ -523,6 +523,26 @@ video {
overflow: hidden;
text-overflow: ellipsis;
}
.topic-header-extra {
margin: 0 0 0 5px;
padding-top: 5px;
}
}
.bullet + .topic-header-extra {
display: block;
line-height: 12px;
}
.bar + .topic-header-extra {
line-height: 1.25;
}
.box + .topic-header-extra {
display: inline-block;
margin: 0 0 0 5px;
padding-top: 5px;
}
/* default docked header CSS for all topics, including those without categories */

View File

@ -133,50 +133,6 @@
}
}
table.group-members {
width: 100%;
p {
max-width: 600px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
th {
padding: 0.5em;
text-align: right;
border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);
}
td.avatar {
width: 60px;
position: relative;
.is-owner {
position: absolute;
right: 0;
top: 20px;
color: dark-light-diff($primary, $secondary, 50%, -50%);
}
}
td.remove-user {
text-align: right;
}
td {
padding: 0.5em;
border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);
img {
margin-right: 10px;
}
span.text {
float: right;
font-size: 1.2em;
color: dark-light-diff($primary, $secondary, 50%, -50%);
}
}
}
.user-right.groups {
margin-top: 0;
}
.user-right {
width: 900px;
margin-top: 20px;

View File

@ -22,6 +22,7 @@
@import "mobile/search";
@import "mobile/emoji";
@import "mobile/ring";
@import "mobile/group";
/* These files doesn't actually exist, they are injected by DiscourseSassImporter. */

View File

@ -16,7 +16,10 @@ display: none !important; // can be removed if inline JS CSS is removed from com
input {
background: $secondary;
color: $primary;
border-color: blend-primary-secondary(15%);
padding: 4px;
border-radius: 3px;
box-shadow: inset 0 1px 1px rgba(0,0,0, .3);
border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);
}
#reply-control {

View File

@ -0,0 +1,49 @@
.group {
margin-top: 15px;
}
.group-header {
margin: 0px;
}
.group-name {
margin: 5px 0px 0px 0px;
}
.group-nav, .group-outlet {
width: 100%;
}
.group-details-container {
margin-bottom: 15px;
}
.group-nav.mobile-nav {
margin-bottom: 15px;
> li {
a {
color: white;
.fa { color: white; }
}
}
background-color: $quaternary;
}
table.group-members {
th {
text-align: center;
}
tr {
.user-info {
width: 130px;
}
td {
padding-left: 0.5em;
}
}
}

View File

@ -19,7 +19,11 @@
color: dark-light-choose(scale-color($primary, $lightness: 35%), scale-color($secondary, $lightness: 65%));
}
label { float: left; display: block; }
textarea, input, select {font-size: 1.143em; clear: left; margin-top: 0; }
textarea, input, select {
font-size: 1.143em;
clear: left;
margin-top: 0;
}
td { padding: 4px; }
}

View File

@ -38,15 +38,16 @@
width: 95%;
}
// an ember metamorph is inserted between btn's sometimes, breaking this rule, but only on mobile for some reason:
// .modal-footer .btn + .btn {
.modal-footer .btn {
// we need a little extra room on mobile for the
// stuff inside the footer to fit
.modal-footer {
padding-right: 0;
}
.modal-footer .btn + .btn {
margin-right: 5px;
margin-bottom: 5px;
}
.modal-footer .btn-group .btn + .btn {
margin-left: -1px;
}
.modal-header {
// we need tighter spacing on mobile for header

View File

@ -36,7 +36,7 @@ class Admin::GroupsController < Admin::AdminController
def create
group = Group.new
group.name = (params[:name] || '').strip
group.name = (group_params[:name] || '').strip
save_group(group)
end
@ -44,29 +44,29 @@ class Admin::GroupsController < Admin::AdminController
group = Group.find(params[:id])
# group rename is ignored for automatic groups
group.name = params[:name] if params[:name] && !group.automatic
group.name = group_params[:name] if group_params[:name] && !group.automatic
save_group(group)
end
def save_group(group)
group.alias_level = params[:alias_level].to_i if params[:alias_level].present?
group.visible = params[:visible] == "true"
grant_trust_level = params[:grant_trust_level].to_i
group.alias_level = group_params[:alias_level].to_i if group_params[:alias_level].present?
group.visible = group_params[:visible] == "true"
grant_trust_level = group_params[:grant_trust_level].to_i
group.grant_trust_level = (grant_trust_level > 0 && grant_trust_level <= 4) ? grant_trust_level : nil
group.automatic_membership_email_domains = params[:automatic_membership_email_domains] unless group.automatic
group.automatic_membership_retroactive = params[:automatic_membership_retroactive] == "true" unless group.automatic
group.automatic_membership_email_domains = group_params[:automatic_membership_email_domains] unless group.automatic
group.automatic_membership_retroactive = group_params[:automatic_membership_retroactive] == "true" unless group.automatic
group.primary_group = group.automatic ? false : params["primary_group"] == "true"
group.primary_group = group.automatic ? false : group_params["primary_group"] == "true"
group.incoming_email = group.automatic ? nil : params[:incoming_email]
group.incoming_email = group.automatic ? nil : group_params[:incoming_email]
title = params[:title] if params[:title].present?
title = group_params[:title] if group_params[:title].present?
group.title = group.automatic ? nil : title
group.flair_url = params[:flair_url].presence
group.flair_bg_color = params[:flair_bg_color].presence
group.flair_color = params[:flair_color].presence
group.flair_url = group_params[:flair_url].presence
group.flair_bg_color = group_params[:flair_bg_color].presence
group.flair_color = group_params[:flair_color].presence
if group.save
Group.reset_counters(group.id, :group_users)
@ -124,7 +124,18 @@ class Admin::GroupsController < Admin::AdminController
protected
def can_not_modify_automatic
render json: {errors: I18n.t('groups.errors.can_not_modify_automatic')}, status: 422
end
def can_not_modify_automatic
render json: {errors: I18n.t('groups.errors.can_not_modify_automatic')}, status: 422
end
private
def group_params
params.require(:group).permit(
:name, :alias_level, :visible, :automatic_membership_email_domains,
:automatic_membership_retroactive, :title, :primary_group,
:grant_trust_level, :incoming_email, :flair_url, :flair_bg_color,
:flair_color
)
end
end

View File

@ -110,6 +110,32 @@ class ApplicationController < ActionController::Base
end
end
def self.last_ar_cache_reset
@last_ar_cache_reset
end
def self.last_ar_cache_reset=(val)
@last_ar_cache_reset
end
rescue_from ActiveRecord::StatementInvalid do |e|
last_cache_reset = ApplicationController.last_ar_cache_reset
if e.message =~ /UndefinedColumn/ && (last_cache_reset.nil? || last_cache_reset < 30.seconds.ago)
Rails.logger.warn "Clear Active Record cache cause schema appears to have changed!"
ApplicationController.last_ar_cache_reset = Time.zone.now
ActiveRecord::Base.connection.query_cache.clear
(ActiveRecord::Base.connection.tables - %w[schema_migrations]).each do |table|
table.classify.constantize.reset_column_information rescue nil
end
end
raise e
end
class PluginDisabled < StandardError; end
# Handles requests for giant IDs that throw pg exceptions
@ -130,7 +156,7 @@ class ApplicationController < ActionController::Base
end
rescue_from Discourse::ReadOnly do
render_json_error I18n.t('read_only_mode_enabled'), type: :read_only, status: 405
render_json_error I18n.t('read_only_mode_enabled'), type: :read_only, status: 503
end
def rescue_discourse_actions(type, status_code, include_ember=false)
@ -382,7 +408,7 @@ class ApplicationController < ActionController::Base
def preload_current_user_data
store_preloaded("currentUser", MultiJson.dump(CurrentUserSerializer.new(current_user, scope: guardian, root: false)))
report = TopicTrackingState.report(current_user.id)
report = TopicTrackingState.report(current_user)
serializer = ActiveModel::ArraySerializer.new(report, each_serializer: TopicTrackingStateSerializer)
store_preloaded("topicTrackingStates", MultiJson.dump(serializer))
end
@ -465,7 +491,7 @@ class ApplicationController < ActionController::Base
end
def mini_profiler_enabled?
defined?(Rack::MiniProfiler) && guardian.is_developer?
defined?(Rack::MiniProfiler) && (guardian.is_developer? || Rails.env.development?)
end
def authorize_mini_profiler

View File

@ -59,6 +59,7 @@ class FinishInstallationController < ApplicationController
end
def ensure_no_admins
preload_anonymous_data
raise Discourse::InvalidAccess.new unless SiteSetting.has_login_hint?
end
end

View File

@ -1,27 +1,26 @@
class GroupsController < ApplicationController
before_filter :ensure_logged_in, only: [:set_notifications, :mentionable]
before_filter :ensure_logged_in, only: [
:set_notifications,
:mentionable,
:update
]
skip_before_filter :preload_json, :check_xhr, only: [:posts_feed, :mentions_feed]
def show
render_serialized(find_group(:id), GroupShowSerializer, root: 'basic_group')
end
def counts
group = find_group(:group_id)
def update
group = Group.find(params[:id])
guardian.ensure_can_edit!(group)
counts = {
posts: group.posts_for(guardian).count,
topics: group.posts_for(guardian).where(post_number: 1).count,
mentions: group.mentioned_posts_for(guardian).count,
members: group.users.count,
}
if guardian.can_see_group_messages?(group)
counts[:messages] = group.messages_for(guardian).where(post_number: 1).count
if group.update_attributes(group_params)
render json: success_json
else
render_json_error(group)
end
render json: { counts: counts }
end
def posts
@ -169,11 +168,21 @@ class GroupsController < ApplicationController
private
def find_group(param_name)
name = params.require(param_name)
group = Group.find_by("lower(name) = ?", name.downcase)
guardian.ensure_can_see!(group)
group
end
def group_params
params.require(:group).permit(
:flair_url,
:flair_bg_color,
:flair_color,
:bio_raw,
:title
)
end
def find_group(param_name)
name = params.require(param_name)
group = Group.find_by("lower(name) = ?", name.downcase)
guardian.ensure_can_see!(group)
group
end
end

View File

@ -6,7 +6,7 @@ class InvitesController < ApplicationController
skip_before_filter :check_xhr, :preload_json
skip_before_filter :redirect_to_login_if_required
before_filter :ensure_logged_in, only: [:destroy, :create, :create_invite_link, :resend_invite, :resend_all_invites, :check_csv_chunk, :upload_csv_chunk]
before_filter :ensure_logged_in, only: [:destroy, :create, :create_invite_link, :resend_invite, :resend_all_invites, :upload_csv]
before_filter :ensure_new_registrations_allowed, only: [:show, :redeem_disposable_invite]
before_filter :ensure_not_logged_in, only: [:show, :redeem_disposable_invite]
@ -147,48 +147,29 @@ class InvitesController < ApplicationController
render nothing: true
end
def check_csv_chunk
def upload_csv
guardian.ensure_can_bulk_invite_to_forum!(current_user)
filename = params.fetch(:resumableFilename)
identifier = params.fetch(:resumableIdentifier)
chunk_number = params.fetch(:resumableChunkNumber)
current_chunk_size = params.fetch(:resumableCurrentChunkSize).to_i
file = params[:file] || params[:files].first
name = params[:name] || File.basename(file.original_filename, ".*")
extension = File.extname(file.original_filename)
# path to chunk file
chunk = Invite.chunk_path(identifier, filename, chunk_number)
# check chunk upload status
status = HandleChunkUpload.check_chunk(chunk, current_chunk_size: current_chunk_size)
render nothing: true, status: status
end
def upload_csv_chunk
guardian.ensure_can_bulk_invite_to_forum!(current_user)
filename = params.fetch(:resumableFilename)
return render status: 415, text: I18n.t("bulk_invite.file_should_be_csv") unless (filename.to_s.end_with?(".csv") || filename.to_s.end_with?(".txt"))
file = params.fetch(:file)
identifier = params.fetch(:resumableIdentifier)
chunk_number = params.fetch(:resumableChunkNumber).to_i
chunk_size = params.fetch(:resumableChunkSize).to_i
total_size = params.fetch(:resumableTotalSize).to_i
current_chunk_size = params.fetch(:resumableCurrentChunkSize).to_i
# path to chunk file
chunk = Invite.chunk_path(identifier, filename, chunk_number)
# upload chunk
HandleChunkUpload.upload_chunk(chunk, file: file)
uploaded_file_size = chunk_number * chunk_size
# when all chunks are uploaded
if uploaded_file_size + current_chunk_size >= total_size
# handle bulk_invite processing in a background thread
Jobs.enqueue(:bulk_invite, filename: filename, identifier: identifier, chunks: chunk_number, current_user_id: current_user.id)
Scheduler::Defer.later("Upload CSV") do
begin
data = if extension == ".csv"
path = Invite.create_csv(file, name)
Jobs.enqueue(:bulk_invite, filename: "#{name}.csv", current_user_id: current_user.id)
{url: path}
else
failed_json.merge(errors: [I18n.t("bulk_invite.file_should_be_csv")])
end
rescue
failed_json.merge(errors: [I18n.t("bulk_invite.error")])
end
MessageBus.publish("/uploads/csv", data.as_json, user_ids: [current_user.id])
end
render nothing: true
render json: success_json
end
def fetch_username

View File

@ -574,7 +574,6 @@ class PostsController < ApplicationController
end
params.require(:raw)
result = params.permit(*permitted).tap do |whitelisted|
whitelisted[:image_sizes] = params[:image_sizes]
# TODO this does not feel right, we should name what meta_data is allowed

Some files were not shown because too many files have changed in this diff Show More