Version bump
This commit is contained in:
commit
a3c836541c
@ -1,45 +0,0 @@
|
||||
# Use this file to configure the Overcommit hooks you wish to use. This will
|
||||
# extend the default configuration defined in:
|
||||
# https://github.com/brigade/overcommit/blob/master/config/default.yml
|
||||
#
|
||||
# At the topmost level of this YAML file is a key representing type of hook
|
||||
# being run (e.g. pre-commit, commit-msg, etc.). Within each type you can
|
||||
# customize each hook, such as whether to only run it on certain files (via
|
||||
# `include`), whether to only display output if it fails (via `quiet`), etc.
|
||||
#
|
||||
# For a complete list of hooks, see:
|
||||
# https://github.com/brigade/overcommit/tree/master/lib/overcommit/hook
|
||||
#
|
||||
# For a complete list of options that you can use to customize hooks, see:
|
||||
# https://github.com/brigade/overcommit#configuration
|
||||
|
||||
PreCommit:
|
||||
RuboCop:
|
||||
enabled: true
|
||||
command: ['bundle', 'exec', 'rubocop']
|
||||
EsLint:
|
||||
enabled: true
|
||||
required_executable: './node_modules/.bin/eslint'
|
||||
install_command: 'yarn install'
|
||||
command: ['yarn', 'eslint', '--ext', '.es6', '-f', 'compact']
|
||||
include: '**/*.es6'
|
||||
YamlSyntax:
|
||||
enabled: true
|
||||
|
||||
PostCheckout:
|
||||
BundleInstall:
|
||||
enabled: true
|
||||
YarnInstall:
|
||||
enabled: true
|
||||
|
||||
PostMerge:
|
||||
BundleInstall:
|
||||
enabled: true
|
||||
YarnInstall:
|
||||
enabled: true
|
||||
|
||||
PostRewrite:
|
||||
BundleInstall:
|
||||
enabled: true
|
||||
YarnInstall:
|
||||
enabled: true
|
||||
@ -74,13 +74,7 @@ script:
|
||||
- |
|
||||
bash -c "
|
||||
if [ '$RUN_LINT' == '1' ]; then
|
||||
bundle exec rubocop --parallel && \
|
||||
yarn prettier --list-different "app/assets/stylesheets/**/*.scss" "app/assets/javascripts/**/*.es6" "test/javascripts/**/*.es6"
|
||||
yarn eslint --ext .es6 app/assets/javascripts && \
|
||||
yarn eslint --ext .es6 test/javascripts && \
|
||||
yarn eslint --ext .es6 plugins/**/assets/javascripts && \
|
||||
yarn eslint --ext .es6 plugins/**/test/javascripts && \
|
||||
yarn eslint app/assets/javascripts test/javascripts
|
||||
npx lefthook run lints
|
||||
else
|
||||
if [ '$QUNIT_RUN' == '1' ]; then
|
||||
bundle exec rake qunit:test['1200000'] && \
|
||||
|
||||
3
Gemfile
3
Gemfile
@ -46,7 +46,7 @@ gem 'redis-namespace'
|
||||
|
||||
gem 'active_model_serializers', '~> 0.8.3'
|
||||
|
||||
gem 'onebox', '1.8.92'
|
||||
gem 'onebox', '1.9.2'
|
||||
|
||||
gem 'http_accept_language', '~>2.0.5', require: false
|
||||
|
||||
@ -152,6 +152,7 @@ group :development do
|
||||
gem 'bullet', require: !!ENV['BULLET']
|
||||
gem 'better_errors'
|
||||
gem 'binding_of_caller'
|
||||
gem 'yaml-lint'
|
||||
|
||||
# waiting on 2.7.5 per: https://github.com/ctran/annotate_models/pull/595
|
||||
if rails_master?
|
||||
|
||||
@ -184,7 +184,7 @@ GEM
|
||||
mini_portile2 (2.4.0)
|
||||
mini_racer (0.2.6)
|
||||
libv8 (>= 6.9.411)
|
||||
mini_scheduler (0.9.2)
|
||||
mini_scheduler (0.11.0)
|
||||
sidekiq
|
||||
mini_sql (0.2.2)
|
||||
mini_suffix (0.3.0)
|
||||
@ -238,7 +238,7 @@ GEM
|
||||
omniauth-twitter (1.4.0)
|
||||
omniauth-oauth (~> 1.1)
|
||||
rack
|
||||
onebox (1.8.92)
|
||||
onebox (1.9.2)
|
||||
htmlentities (~> 4.3)
|
||||
moneta (~> 1.0)
|
||||
multi_json (~> 1.11)
|
||||
@ -412,6 +412,7 @@ GEM
|
||||
webpush (0.3.8)
|
||||
hkdf (~> 0.2)
|
||||
jwt (~> 2.0)
|
||||
yaml-lint (0.0.10)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
@ -486,7 +487,7 @@ DEPENDENCIES
|
||||
omniauth-oauth2
|
||||
omniauth-openid
|
||||
omniauth-twitter
|
||||
onebox (= 1.8.92)
|
||||
onebox (= 1.9.2)
|
||||
openid-redis-store
|
||||
parallel_tests
|
||||
pg
|
||||
@ -533,6 +534,7 @@ DEPENDENCIES
|
||||
unicorn
|
||||
webmock
|
||||
webpush
|
||||
yaml-lint
|
||||
|
||||
BUNDLED WITH
|
||||
1.17.3
|
||||
|
||||
@ -17,8 +17,9 @@ export default Ember.Component.extend({
|
||||
|
||||
@observes("content")
|
||||
contentChanged() {
|
||||
if (this._editor && !this._skipContentChangeEvent && this.content) {
|
||||
this._editor.getSession().setValue(this.content);
|
||||
const content = this.content || "";
|
||||
if (this._editor && !this._skipContentChangeEvent) {
|
||||
this._editor.getSession().setValue(content);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@ -55,7 +55,6 @@ export default Ember.Component.extend({
|
||||
showTitle: true,
|
||||
showFilteringUI: false,
|
||||
showDatesOptions: Ember.computed.alias("model.dates_filtering"),
|
||||
showExport: Ember.computed.not("model.onlyTable"),
|
||||
showRefresh: Ember.computed.or(
|
||||
"showDatesOptions",
|
||||
"model.available_filters.length"
|
||||
@ -165,8 +164,8 @@ export default Ember.Component.extend({
|
||||
let reportKey = "reports:";
|
||||
reportKey += [
|
||||
dataSourceName,
|
||||
startDate.replace(/-/g, ""),
|
||||
endDate.replace(/-/g, ""),
|
||||
Ember.testing ? "start" : startDate.replace(/-/g, ""),
|
||||
Ember.testing ? "end" : endDate.replace(/-/g, ""),
|
||||
"[:prev_period]",
|
||||
this.get("reportOptions.table.limit"),
|
||||
customFilters
|
||||
|
||||
@ -1,12 +1,17 @@
|
||||
import BufferedContent from "discourse/mixins/buffered-content";
|
||||
import SettingComponent from "admin/mixins/setting-component";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
|
||||
export default Ember.Component.extend(BufferedContent, SettingComponent, {
|
||||
layoutName: "admin/templates/components/site-setting",
|
||||
_save() {
|
||||
return this.model.saveSettings(
|
||||
this.get("setting.setting"),
|
||||
this.get("buffered.value")
|
||||
);
|
||||
return ajax(`/admin/themes/${this.model.id}/setting`, {
|
||||
type: "PUT",
|
||||
data: {
|
||||
name: this.setting.setting,
|
||||
value: this.get("buffered.value")
|
||||
}
|
||||
}).catch(popupAjaxError);
|
||||
}
|
||||
});
|
||||
|
||||
@ -2,6 +2,8 @@ import {
|
||||
default as computed,
|
||||
observes
|
||||
} from "ember-addons/ember-computed-decorators";
|
||||
import { iconHTML } from "discourse-common/lib/icon-library";
|
||||
import { escape } from "pretty-text/sanitizer";
|
||||
|
||||
const MAX_COMPONENTS = 4;
|
||||
|
||||
@ -64,7 +66,10 @@ export default Ember.Component.extend({
|
||||
children = this.childrenExpanded
|
||||
? children
|
||||
: children.slice(0, MAX_COMPONENTS);
|
||||
return children.map(t => t.get("name"));
|
||||
return children.map(t => {
|
||||
const name = escape(t.name);
|
||||
return t.enabled ? name : `${iconHTML("ban")} ${name}`;
|
||||
});
|
||||
},
|
||||
|
||||
@computed("children")
|
||||
|
||||
@ -5,6 +5,13 @@ import computed from "ember-addons/ember-computed-decorators";
|
||||
export default Ember.Controller.extend(bufferedProperty("emailTemplate"), {
|
||||
saved: false,
|
||||
|
||||
@computed("buffered.body", "buffered.subject")
|
||||
saveDisabled(body, subject) {
|
||||
return (
|
||||
this.emailTemplate.body === body && this.emailTemplate.subject === subject
|
||||
);
|
||||
},
|
||||
|
||||
@computed("buffered")
|
||||
hasMultipleSubjects(buffered) {
|
||||
if (buffered.getProperties("subject")["subject"]) {
|
||||
|
||||
@ -301,6 +301,20 @@ export default Ember.Controller.extend({
|
||||
} else {
|
||||
this.commitSwitchType();
|
||||
}
|
||||
},
|
||||
|
||||
enableComponent() {
|
||||
this.model.set("enabled", true);
|
||||
this.model
|
||||
.saveChanges("enabled")
|
||||
.catch(() => this.model.set("enabled", false));
|
||||
},
|
||||
|
||||
disableComponent() {
|
||||
this.model.set("enabled", false);
|
||||
this.model
|
||||
.saveChanges("enabled")
|
||||
.catch(() => this.model.set("enabled", true));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,9 +1,15 @@
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { bufferedProperty } from "discourse/mixins/buffered-content";
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
|
||||
export default Ember.Controller.extend(bufferedProperty("siteText"), {
|
||||
saved: false,
|
||||
|
||||
@computed("buffered.value")
|
||||
saveDisabled(value) {
|
||||
return this.siteText.value === value;
|
||||
},
|
||||
|
||||
actions: {
|
||||
saveChanges() {
|
||||
const buffered = this.buffered;
|
||||
|
||||
@ -15,32 +15,64 @@ export default Ember.Controller.extend(CanCheckEmails, {
|
||||
selectAll: false,
|
||||
searchHint: i18n("search_hint"),
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
|
||||
this._page = 1;
|
||||
this._results = [];
|
||||
this._canLoadMore = true;
|
||||
},
|
||||
|
||||
@computed("query")
|
||||
title(query) {
|
||||
return I18n.t("admin.users.titles." + query);
|
||||
},
|
||||
|
||||
_filterUsers: debounce(function() {
|
||||
this._refreshUsers();
|
||||
this.resetFilters();
|
||||
}, 250).observes("listFilter"),
|
||||
|
||||
resetFilters() {
|
||||
this._page = 1;
|
||||
this._results = [];
|
||||
this._canLoadMore = true;
|
||||
this._refreshUsers();
|
||||
},
|
||||
|
||||
_refreshUsers() {
|
||||
if (!this._canLoadMore) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.set("refreshing", true);
|
||||
|
||||
AdminUser.findAll(this.query, {
|
||||
filter: this.listFilter,
|
||||
show_emails: this.showEmails,
|
||||
order: this.order,
|
||||
ascending: this.ascending
|
||||
ascending: this.ascending,
|
||||
page: this._page
|
||||
})
|
||||
.then(result => this.set("model", result))
|
||||
.then(result => {
|
||||
if (!result || result.length === 0) {
|
||||
this._canLoadMore = false;
|
||||
}
|
||||
|
||||
this._results = this._results.concat(result);
|
||||
this.set("model", this._results);
|
||||
})
|
||||
.finally(() => this.set("refreshing", false));
|
||||
},
|
||||
|
||||
actions: {
|
||||
loadMore() {
|
||||
this._page += 1;
|
||||
this._refreshUsers();
|
||||
},
|
||||
|
||||
toggleEmailVisibility() {
|
||||
this.toggleProperty("showEmails");
|
||||
this._refreshUsers();
|
||||
this.resetFilters();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -17,6 +17,8 @@ const CUSTOM_TYPES = [
|
||||
"group_list"
|
||||
];
|
||||
|
||||
const AUTO_REFRESH_ON_SAVE = ["logo", "logo_small", "large_icon"];
|
||||
|
||||
export default Ember.Mixin.create({
|
||||
classNameBindings: [":row", ":setting", "overridden", "typeClass"],
|
||||
content: Ember.computed.alias("setting"),
|
||||
@ -113,7 +115,9 @@ export default Ember.Mixin.create({
|
||||
.then(() => {
|
||||
this.set("validationMessage", null);
|
||||
this.commitBuffer();
|
||||
this.afterSave();
|
||||
if (AUTO_REFRESH_ON_SAVE.includes(this.get("setting.setting"))) {
|
||||
this.afterSave();
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
if (e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors) {
|
||||
|
||||
@ -16,8 +16,8 @@ const Report = Discourse.Model.extend({
|
||||
higher_is_better: true,
|
||||
|
||||
@computed("modes")
|
||||
onlyTable(modes) {
|
||||
return modes.length === 1 && modes[0] === "table";
|
||||
isTable(modes) {
|
||||
return modes.some(mode => mode === "table");
|
||||
},
|
||||
|
||||
@computed("type", "start_date", "end_date")
|
||||
@ -332,9 +332,7 @@ const Report = Discourse.Model.extend({
|
||||
ignoreTitle: true
|
||||
});
|
||||
|
||||
return `<a href='${href}'>${avatarImg}<span class='username'>${
|
||||
user.name
|
||||
}</span></a>`;
|
||||
return `<a href='${href}'>${avatarImg}<span class='username'>${user.name}</span></a>`;
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@ -21,7 +21,7 @@ export default Discourse.Route.extend({
|
||||
refreshing: false
|
||||
});
|
||||
|
||||
controller._refreshUsers();
|
||||
controller.resetFilters();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -164,7 +164,7 @@
|
||||
</div>
|
||||
{{/each}}
|
||||
|
||||
{{#if showExport}}
|
||||
{{#if model.isTable}}
|
||||
<div class="control">
|
||||
<div class="input">
|
||||
{{d-button
|
||||
|
||||
@ -17,6 +17,9 @@
|
||||
{{#if theme.isBroken}}
|
||||
{{d-icon "exclamation-circle" class="broken-indicator" title="admin.customize.theme.broken_theme_tooltip"}}
|
||||
{{/if}}
|
||||
{{#unless theme.enabled}}
|
||||
{{d-icon "ban" class="light-grey-icon" title="admin.customize.theme.disabled_component_tooltip"}}
|
||||
{{/unless}}
|
||||
{{else}}
|
||||
{{d-icon "caret-right"}}
|
||||
{{/unless}}
|
||||
@ -25,7 +28,7 @@
|
||||
|
||||
{{#if displayComponents}}
|
||||
<div class="components-list">
|
||||
<span class="components">{{childrenString}}</span>
|
||||
<span class="components">{{{childrenString}}}</span>
|
||||
|
||||
{{#if displayHasMore}}
|
||||
<span {{action "toggleChildrenExpanded"}} class="others-count">
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
<label>{{i18n "admin.customize.email_templates.body"}}</label>
|
||||
{{d-editor value=buffered.body}}
|
||||
|
||||
{{#save-controls model=emailTemplate action=(action "saveChanges") saved=saved}}
|
||||
{{#save-controls model=emailTemplate action=(action "saveChanges") saved=saved saveDisabled=saveDisabled}}
|
||||
{{#if emailTemplate.can_revert}}
|
||||
{{d-button action=(action "revertChanges") label="admin.customize.email_templates.revert"}}
|
||||
{{/if}}
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
</div>
|
||||
{{/each}}
|
||||
|
||||
{{#unless model.enabled}}
|
||||
{{#unless model.supported}}
|
||||
<div class="alert alert-error">
|
||||
{{i18n "admin.customize.theme.required_version.error"}}
|
||||
{{#if model.remote_theme.minimum_discourse_version}}
|
||||
@ -28,6 +28,26 @@
|
||||
</div>
|
||||
{{/unless}}
|
||||
|
||||
{{#unless model.enabled}}
|
||||
<div class="alert alert-error">
|
||||
{{#if model.disabled_by}}
|
||||
{{i18n "admin.customize.theme.disabled_by"}}
|
||||
{{#user-link user=model.disabled_by}}
|
||||
{{avatar model.disabled_by imageSize="tiny"}}
|
||||
{{model.disabled_by.username}}
|
||||
{{/user-link}}
|
||||
{{format-date model.disabled_at leaveAgo="true"}}
|
||||
{{else}}
|
||||
{{i18n "admin.customize.theme.disabled"}}
|
||||
{{/if}}
|
||||
{{d-button
|
||||
class='btn-default'
|
||||
action=(action "enableComponent")
|
||||
icon="check"
|
||||
label="admin.customize.theme.enable"}}
|
||||
</div>
|
||||
{{/unless}}
|
||||
|
||||
{{#unless model.component}}
|
||||
<div class="control-unit">
|
||||
{{inline-edit-checkbox action=(action "applyDefault") labelKey="admin.customize.theme.is_default" checked=model.default}}
|
||||
@ -204,7 +224,13 @@
|
||||
{{#if model.childThemes.length}}
|
||||
<ul class='removable-list'>
|
||||
{{#each model.childThemes as |child|}}
|
||||
<li>{{#link-to 'adminCustomizeThemes.show' child replace=true class='col'}}{{child.name}}{{/link-to}} {{d-button action=(action "removeChildTheme") actionParam=child class="btn-default cancel-edit col" icon="times"}}</li>
|
||||
<li class={{unless child.enabled "disabled-child"}}>
|
||||
{{#link-to 'adminCustomizeThemes.show' child replace=true class='col child-link'}}
|
||||
{{child.name}}
|
||||
{{/link-to}}
|
||||
|
||||
{{d-button action=(action "removeChildTheme") actionParam=child class="btn-default cancel-edit col" icon="times"}}
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{/if}}
|
||||
@ -221,5 +247,22 @@
|
||||
<a class="btn btn-default export" target="_blank" href={{downloadUrl}}>{{d-icon "download"}} {{i18n 'admin.export_json.button_text'}}</a>
|
||||
|
||||
{{d-button action=(action "switchType") label="admin.customize.theme.convert" icon=convertIcon class="btn-default btn-normal" title=convertTooltip}}
|
||||
|
||||
{{#if model.component}}
|
||||
{{#if model.enabled}}
|
||||
{{d-button
|
||||
class='btn-default'
|
||||
action=(action "disableComponent")
|
||||
icon="ban"
|
||||
label="admin.customize.theme.disable"}}
|
||||
{{else}}
|
||||
{{d-button
|
||||
class='btn-default'
|
||||
action=(action "enableComponent")
|
||||
icon="check"
|
||||
label="admin.customize.theme.enable"}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{d-button action=(action "destroy") label="admin.customize.delete" icon="trash-alt" class="btn-danger"}}
|
||||
</div>
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
|
||||
{{expanding-text-area value=buffered.value rows="1" class="site-text-value"}}
|
||||
|
||||
{{#save-controls model=siteText action=(action "saveChanges") saved=saved}}
|
||||
{{#save-controls model=siteText action=(action "saveChanges") saved=saved saveDisabled=saveDisabled}}
|
||||
{{#if siteText.can_revert}}
|
||||
{{d-button action=(action "revertChanges") label="admin.site_text.revert" class="revert-site-text"}}
|
||||
{{/if}}
|
||||
|
||||
@ -367,7 +367,11 @@
|
||||
<div class="display-row">
|
||||
<div class="field">{{i18n "trust_level"}}</div>
|
||||
<div class="value">
|
||||
{{combo-box content=site.trustLevels value=model.trust_level nameProperty="detailedName"}}
|
||||
{{combo-box
|
||||
content=site.trustLevels
|
||||
value=model.trustLevel.id
|
||||
nameProperty="detailedName"}}
|
||||
|
||||
{{#if model.dirty}}
|
||||
<div>
|
||||
{{d-button class="ok no-text" action=(action "saveTrustLevel") icon="check"}}
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
|
||||
</div>
|
||||
|
||||
{{#conditional-loading-spinner condition=refreshing}}
|
||||
{{#load-more selector=".users-list tr" action=(action "loadMore")}}
|
||||
{{#if model}}
|
||||
<table class='table users-list grid'>
|
||||
<thead>
|
||||
@ -90,8 +90,9 @@
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{conditional-loading-spinner condition=refreshing}}
|
||||
|
||||
{{else}}
|
||||
<p>{{i18n 'search.no_results'}}</p>
|
||||
{{/if}}
|
||||
{{/conditional-loading-spinner}}
|
||||
{{/load-more}}
|
||||
|
||||
@ -10,9 +10,9 @@
|
||||
{{nav-item route='adminUsersList.show' routeParam='staged' label='admin.users.nav.staged'}}
|
||||
{{nav-item route='groups' label='groups.index.title'}}
|
||||
<div class="admin-actions">
|
||||
{{#unless siteSettings.enable_sso}}
|
||||
{{#if currentUser.can_invite_to_forum}}
|
||||
{{d-button class="btn-default" action=(route-action "sendInvites") title="admin.invite.button_title" icon="user-plus" label="admin.invite.button_text"}}
|
||||
{{/unless}}
|
||||
{{/if}}
|
||||
{{#if currentUser.admin}}
|
||||
{{d-button class="btn-default" action=(route-action "exportUsers") title="admin.export_csv.button_title.user" icon="download" label="admin.export_csv.button_text"}}
|
||||
{{/if}}
|
||||
|
||||
@ -72,9 +72,7 @@ export default Ember.Component.extend({
|
||||
},
|
||||
|
||||
_formatReplyToUserPost(avatar, link) {
|
||||
const htmlLink = `<a class="user-link" href="${link.href}">${
|
||||
link.anchor
|
||||
}</a>`;
|
||||
const htmlLink = `<a class="user-link" href="${link.href}">${link.anchor}</a>`;
|
||||
return `${avatar}${htmlLink}`.htmlSafe();
|
||||
}
|
||||
});
|
||||
|
||||
@ -541,7 +541,7 @@ export default Ember.Component.extend({
|
||||
}
|
||||
textarea.selectionStart = from;
|
||||
textarea.selectionEnd = from + length;
|
||||
|
||||
Ember.run.next(() => $textarea.trigger("change"));
|
||||
$textarea.scrollTop(oldScrollPos);
|
||||
});
|
||||
},
|
||||
@ -601,7 +601,13 @@ export default Ember.Component.extend({
|
||||
this.set("value", `${pre}${hval}${example}${tail}${post}`);
|
||||
this._selectText(pre.length + hlen, example.length);
|
||||
} else if (opts && !opts.multiline) {
|
||||
const [hval, hlen] = getHead(head);
|
||||
let [hval, hlen] = getHead(head);
|
||||
|
||||
if (opts.useBlockMode && sel.value.split("\n").length > 1) {
|
||||
hval += "\n";
|
||||
hlen += 1;
|
||||
tail = `\n${tail}`;
|
||||
}
|
||||
|
||||
if (pre.slice(-hlen) === hval && post.slice(0, tail.length) === tail) {
|
||||
this.set(
|
||||
|
||||
@ -5,6 +5,8 @@ import {
|
||||
on
|
||||
} from "ember-addons/ember-computed-decorators";
|
||||
|
||||
const DATE_FORMAT = "YYYY-MM-DD";
|
||||
|
||||
export default Ember.Component.extend({
|
||||
classNames: ["date-picker-wrapper"],
|
||||
_picker: null,
|
||||
@ -17,11 +19,10 @@ export default Ember.Component.extend({
|
||||
|
||||
@on("didInsertElement")
|
||||
_loadDatePicker() {
|
||||
const container = this.element.querySelector(`#${this.containerId}`);
|
||||
|
||||
if (this.site.mobileView) {
|
||||
this._loadNativePicker(container);
|
||||
this._loadNativePicker();
|
||||
} else {
|
||||
const container = document.getElementById(this.containerId);
|
||||
this._loadPikadayPicker(container);
|
||||
}
|
||||
},
|
||||
@ -29,11 +30,11 @@ export default Ember.Component.extend({
|
||||
_loadPikadayPicker(container) {
|
||||
loadScript("/javascripts/pikaday.js").then(() => {
|
||||
Ember.run.next(() => {
|
||||
const default_opts = {
|
||||
const options = {
|
||||
field: this.element.querySelector(".date-picker"),
|
||||
container: container || this.element,
|
||||
container: container || null,
|
||||
bound: container === null,
|
||||
format: "YYYY-MM-DD",
|
||||
format: DATE_FORMAT,
|
||||
firstDay: 1,
|
||||
i18n: {
|
||||
previousMonth: I18n.t("dates.previous_month"),
|
||||
@ -45,14 +46,13 @@ export default Ember.Component.extend({
|
||||
onSelect: date => this._handleSelection(date)
|
||||
};
|
||||
|
||||
this._picker = new Pikaday(Object.assign(default_opts, this._opts()));
|
||||
this._picker = new Pikaday(Object.assign(options, this._opts()));
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
_loadNativePicker(container) {
|
||||
const wrapper = container || this.element;
|
||||
const picker = wrapper.querySelector("input.date-picker");
|
||||
_loadNativePicker() {
|
||||
const picker = this.element.querySelector("input.date-picker");
|
||||
picker.onchange = () => this._handleSelection(picker.value);
|
||||
picker.hide = () => {
|
||||
/* do nothing for native */
|
||||
@ -64,12 +64,10 @@ export default Ember.Component.extend({
|
||||
},
|
||||
|
||||
_handleSelection(value) {
|
||||
const formattedDate = moment(value).format("YYYY-MM-DD");
|
||||
const formattedDate = moment(value).format(DATE_FORMAT);
|
||||
|
||||
if (!this.element || this.isDestroying || this.isDestroyed) return;
|
||||
|
||||
this._picker && this._picker.hide();
|
||||
|
||||
if (this.onSelect) {
|
||||
this.onSelect(formattedDate);
|
||||
}
|
||||
@ -79,8 +77,8 @@ export default Ember.Component.extend({
|
||||
_destroy() {
|
||||
if (this._picker) {
|
||||
this._picker.destroy();
|
||||
this._picker = null;
|
||||
}
|
||||
this._picker = null;
|
||||
},
|
||||
|
||||
@computed()
|
||||
|
||||
@ -565,7 +565,7 @@ export default Ember.Component.extend({
|
||||
} else {
|
||||
const previewInputOffset = $(".d-editor-input").offset();
|
||||
|
||||
const pickerHeight = $(".emoji-picker").height();
|
||||
const pickerHeight = $(".d-editor .emoji-picker").height();
|
||||
const editorHeight = $(".d-editor-input").height();
|
||||
const windowBottom = $(window).scrollTop() + $(window).height();
|
||||
|
||||
|
||||
@ -13,19 +13,25 @@ export default Ember.Component.extend({
|
||||
},
|
||||
{
|
||||
name: I18n.t(
|
||||
"admin.groups.manage.interaction.visibility_levels.members"
|
||||
"admin.groups.manage.interaction.visibility_levels.logged_on_users"
|
||||
),
|
||||
value: 1
|
||||
},
|
||||
{
|
||||
name: I18n.t("admin.groups.manage.interaction.visibility_levels.staff"),
|
||||
name: I18n.t(
|
||||
"admin.groups.manage.interaction.visibility_levels.members"
|
||||
),
|
||||
value: 2
|
||||
},
|
||||
{
|
||||
name: I18n.t("admin.groups.manage.interaction.visibility_levels.staff"),
|
||||
value: 3
|
||||
},
|
||||
{
|
||||
name: I18n.t(
|
||||
"admin.groups.manage.interaction.visibility_levels.owners"
|
||||
),
|
||||
value: 3
|
||||
value: 4
|
||||
}
|
||||
];
|
||||
|
||||
@ -34,6 +40,7 @@ export default Ember.Component.extend({
|
||||
{ name: I18n.t("groups.alias_levels.only_admins"), value: 1 },
|
||||
{ name: I18n.t("groups.alias_levels.mods_and_admins"), value: 2 },
|
||||
{ name: I18n.t("groups.alias_levels.members_mods_and_admins"), value: 3 },
|
||||
{ name: I18n.t("groups.alias_levels.owners_mods_and_admins"), value: 4 },
|
||||
{ name: I18n.t("groups.alias_levels.everyone"), value: 99 }
|
||||
];
|
||||
},
|
||||
|
||||
@ -9,7 +9,9 @@ export default Ember.Component.extend({
|
||||
relatedTitle(topic) {
|
||||
const href = this.currentUser && this.currentUser.pmPath(topic);
|
||||
return href
|
||||
? `<a href="${href}">${iconHTML("envelope", {
|
||||
? `<a href="${href}" aria-label="${I18n.t(
|
||||
"user.messages.inbox"
|
||||
)}">${iconHTML("envelope", {
|
||||
class: "private-message-glyph"
|
||||
})}</a><span>${I18n.t("related_messages.title")}</span>`
|
||||
: I18n.t("related_messages.title");
|
||||
|
||||
@ -10,7 +10,9 @@ export default Ember.Component.extend({
|
||||
suggestedTitle(topic) {
|
||||
const href = this.currentUser && this.currentUser.pmPath(topic);
|
||||
return topic.get("isPrivateMessage") && href
|
||||
? `<a href="${href}">${iconHTML("envelope", {
|
||||
? `<a href="${href}" aria-label="${I18n.t(
|
||||
"user.messages.inbox"
|
||||
)}>${iconHTML("envelope", {
|
||||
class: "private-message-glyph"
|
||||
})}</a><span>${I18n.t("suggested_topics.pm_title")}</span>`
|
||||
: I18n.t("suggested_topics.title");
|
||||
|
||||
@ -0,0 +1,17 @@
|
||||
import { default as computed } from "ember-addons/ember-computed-decorators";
|
||||
|
||||
export default Ember.Component.extend({
|
||||
classNames: ["topic-notice"],
|
||||
|
||||
@computed("model.group.{full_name,name,allow_membership_requests}")
|
||||
accessViaGroupText(group) {
|
||||
const name = group.full_name || group.name;
|
||||
const suffix = group.allow_membership_requests ? "request" : "join";
|
||||
return I18n.t(`topic.group_${suffix}`, { name });
|
||||
},
|
||||
|
||||
@computed("model.group.allow_membership_requests")
|
||||
accessViaGroupButtonText(allowRequest) {
|
||||
return `groups.${allowRequest ? "request" : "join"}`;
|
||||
}
|
||||
});
|
||||
@ -1,6 +1,8 @@
|
||||
import { iconHTML } from "discourse-common/lib/icon-library";
|
||||
import { bufferedRender } from "discourse-common/lib/buffered-render";
|
||||
import Category from "discourse/models/category";
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
import { REMINDER_TYPE } from "discourse/controllers/edit-topic-timer";
|
||||
|
||||
export default Ember.Component.extend(
|
||||
bufferedRender({
|
||||
@ -16,6 +18,12 @@ export default Ember.Component.extend(
|
||||
"categoryId"
|
||||
],
|
||||
|
||||
@computed("statusType")
|
||||
canRemoveTimer(type) {
|
||||
if (type === REMINDER_TYPE) return true;
|
||||
return this.currentUser && this.currentUser.get("canManageTopic");
|
||||
},
|
||||
|
||||
buildBuffer(buffer) {
|
||||
if (!this.executeAt) return;
|
||||
|
||||
@ -40,7 +48,7 @@ export default Ember.Component.extend(
|
||||
}
|
||||
let autoCloseHours = this.duration || 0;
|
||||
|
||||
buffer.push(`<h3>${iconHTML("far-clock")} `);
|
||||
buffer.push(`<h3 class="topic-timer-heading">`);
|
||||
|
||||
let options = {
|
||||
timeLeft: duration.humanize(true),
|
||||
@ -61,11 +69,17 @@ export default Ember.Component.extend(
|
||||
}
|
||||
|
||||
buffer.push(
|
||||
`<span title="${moment(this.executeAt).format("LLLL")}">${I18n.t(
|
||||
this._noticeKey(),
|
||||
options
|
||||
)}</span>`
|
||||
`<span title="${moment(this.executeAt).format("LLLL")}">${iconHTML(
|
||||
"far-clock"
|
||||
)} ${I18n.t(this._noticeKey(), options)}</span>`
|
||||
);
|
||||
if (this.removeTopicTimer && this.canRemoveTimer) {
|
||||
buffer.push(
|
||||
`<button class="btn topic-timer-remove no-text" title="${I18n.t(
|
||||
"post.controls.remove_timer"
|
||||
)}">${iconHTML("trash-alt")}</button>`
|
||||
);
|
||||
}
|
||||
buffer.push("</h3>");
|
||||
|
||||
// TODO Sam: concerned this can cause a heavy rerender loop
|
||||
@ -79,7 +93,21 @@ export default Ember.Component.extend(
|
||||
}
|
||||
},
|
||||
|
||||
didInsertElement() {
|
||||
this._super(...arguments);
|
||||
|
||||
if (this.removeTopicTimer) {
|
||||
$(this.element).on(
|
||||
"click.topic-timer-remove",
|
||||
"button",
|
||||
this.removeTopicTimer
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
$(this.element).off("click.topic-timer-remove", this.removeTopicTimer);
|
||||
|
||||
if (this._delayedRerender) {
|
||||
Ember.run.cancel(this._delayedRerender);
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
import { cookAsync } from "discourse/lib/text";
|
||||
|
||||
export default Ember.Controller.extend(ModalFunctionality, {
|
||||
post: null,
|
||||
@ -42,10 +43,11 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
|
||||
post
|
||||
.updatePostField("notice", notice)
|
||||
.then(() => {
|
||||
.then(() => cookAsync(notice, { features: { onebox: false } }))
|
||||
.then(cookedNotice => {
|
||||
post.setProperties({
|
||||
notice_type: "custom",
|
||||
notice_args: notice
|
||||
notice_args: cookedNotice.string
|
||||
});
|
||||
resolve();
|
||||
this.send("closeModal");
|
||||
|
||||
@ -507,7 +507,9 @@ export default Ember.Controller.extend({
|
||||
},
|
||||
|
||||
cancel() {
|
||||
this.cancelComposer();
|
||||
const differentDraftContext =
|
||||
this.get("topic.id") !== this.get("model.topic.id");
|
||||
this.cancelComposer(differentDraftContext);
|
||||
},
|
||||
|
||||
save() {
|
||||
@ -830,7 +832,12 @@ export default Ember.Controller.extend({
|
||||
}
|
||||
|
||||
// If it's a different draft, cancel it and try opening again.
|
||||
return this.cancelComposer()
|
||||
const differentDraftContext =
|
||||
opts.post && composerModel.topic
|
||||
? composerModel.topic.id !== opts.post.topic_id
|
||||
: true;
|
||||
|
||||
return this.cancelComposer(differentDraftContext)
|
||||
.then(() => this.open(opts))
|
||||
.then(resolve, reject);
|
||||
}
|
||||
@ -984,11 +991,23 @@ export default Ember.Controller.extend({
|
||||
}
|
||||
},
|
||||
|
||||
cancelComposer() {
|
||||
cancelComposer(differentDraft = false) {
|
||||
return new Ember.RSVP.Promise(resolve => {
|
||||
if (this.get("model.hasMetaData") || this.get("model.replyDirty")) {
|
||||
bootbox.dialog(I18n.t("post.abandon.confirm"), [
|
||||
{ label: I18n.t("post.abandon.no_value") },
|
||||
{
|
||||
label: differentDraft
|
||||
? I18n.t("post.abandon.no_save_draft")
|
||||
: I18n.t("post.abandon.no_value"),
|
||||
callback: () => {
|
||||
// cancel composer without destroying draft on new draft context
|
||||
if (differentDraft) {
|
||||
this.model.clearState();
|
||||
this.close();
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: I18n.t("post.abandon.yes_value"),
|
||||
class: "btn-danger",
|
||||
|
||||
@ -77,6 +77,10 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
keys2: [SHIFT, "k"],
|
||||
keysDelimiter: PLUS,
|
||||
shortcutsDelimiter: "slash"
|
||||
}),
|
||||
go_to_unread_post: buildShortcut("navigation.go_to_unread_post", {
|
||||
keys1: [SHIFT, "l"],
|
||||
keysDelimiter: PLUS
|
||||
})
|
||||
},
|
||||
application: {
|
||||
@ -158,6 +162,10 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
print: buildShortcut("actions.print", {
|
||||
keys1: [CTRL, "p"],
|
||||
keysDelimiter: PLUS
|
||||
}),
|
||||
defer: buildShortcut("actions.defer", {
|
||||
keys1: [SHIFT, "u"],
|
||||
keysDelimiter: PLUS
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,37 +1,28 @@
|
||||
import { default as computed } from "ember-addons/ember-computed-decorators";
|
||||
import CanCheckEmails from "discourse/mixins/can-check-emails";
|
||||
import { default as DiscourseURL, userPath } from "discourse/lib/url";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { findAll } from "discourse/models/login-method";
|
||||
import { SECOND_FACTOR_METHODS } from "discourse/models/user";
|
||||
import showModal from "discourse/lib/show-modal";
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
export default Ember.Controller.extend(CanCheckEmails, {
|
||||
loading: false,
|
||||
dirty: false,
|
||||
resetPasswordLoading: false,
|
||||
resetPasswordProgress: "",
|
||||
password: null,
|
||||
secondFactorImage: null,
|
||||
secondFactorKey: null,
|
||||
showSecondFactorKey: false,
|
||||
errorMessage: null,
|
||||
newUsername: null,
|
||||
backupEnabled: Ember.computed.alias("model.second_factor_backup_enabled"),
|
||||
secondFactorMethod: SECOND_FACTOR_METHODS.TOTP,
|
||||
totps: null,
|
||||
|
||||
loaded: Ember.computed.and("secondFactorImage", "secondFactorKey"),
|
||||
|
||||
@computed("loading")
|
||||
submitButtonText(loading) {
|
||||
return loading ? "loading" : "continue";
|
||||
},
|
||||
|
||||
@computed("loading")
|
||||
enableButtonText(loading) {
|
||||
return loading ? "loading" : "enable";
|
||||
},
|
||||
|
||||
@computed("loading")
|
||||
disableButtonText(loading) {
|
||||
return loading ? "loading" : "disable";
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.set("totps", []);
|
||||
},
|
||||
|
||||
@computed
|
||||
@ -41,58 +32,64 @@ export default Ember.Controller.extend({
|
||||
|
||||
@computed("currentUser")
|
||||
showEnforcedNotice(user) {
|
||||
return user && user.get("enforcedSecondFactor");
|
||||
return user && user.enforcedSecondFactor;
|
||||
},
|
||||
|
||||
toggleSecondFactor(enable) {
|
||||
if (!this.secondFactorToken) return;
|
||||
handleError(error) {
|
||||
if (error.jqXHR) {
|
||||
error = error.jqXHR;
|
||||
}
|
||||
let parsedJSON = error.responseJSON;
|
||||
if (parsedJSON.error_type === "invalid_access") {
|
||||
const usernameLower = this.model.username.toLowerCase();
|
||||
DiscourseURL.redirectTo(
|
||||
userPath(`${usernameLower}/preferences/second-factor`)
|
||||
);
|
||||
} else {
|
||||
popupAjaxError(error);
|
||||
}
|
||||
},
|
||||
|
||||
loadSecondFactors() {
|
||||
if (this.dirty === false) {
|
||||
return;
|
||||
}
|
||||
this.set("loading", true);
|
||||
|
||||
this.model
|
||||
.toggleSecondFactor(
|
||||
this.secondFactorToken,
|
||||
this.secondFactorMethod,
|
||||
SECOND_FACTOR_METHODS.TOTP,
|
||||
enable
|
||||
)
|
||||
.loadSecondFactorCodes(this.password)
|
||||
.then(response => {
|
||||
if (response.error) {
|
||||
this.set("errorMessage", response.error);
|
||||
return;
|
||||
}
|
||||
|
||||
this.set("errorMessage", null);
|
||||
DiscourseURL.redirectTo(
|
||||
userPath(`${this.model.username.toLowerCase()}/preferences`)
|
||||
this.setProperties({
|
||||
errorMessage: null,
|
||||
loaded: true,
|
||||
totps: response.totps,
|
||||
password: null,
|
||||
dirty: false
|
||||
});
|
||||
this.set(
|
||||
"model.second_factor_enabled",
|
||||
response.totps && response.totps.length > 0
|
||||
);
|
||||
})
|
||||
.catch(error => {
|
||||
popupAjaxError(error);
|
||||
})
|
||||
.catch(e => this.handleError(e))
|
||||
.finally(() => this.set("loading", false));
|
||||
},
|
||||
|
||||
markDirty() {
|
||||
this.set("dirty", true);
|
||||
},
|
||||
|
||||
actions: {
|
||||
confirmPassword() {
|
||||
if (!this.password) return;
|
||||
this.set("loading", true);
|
||||
|
||||
this.model
|
||||
.loadSecondFactorCodes(this.password)
|
||||
.then(response => {
|
||||
if (response.error) {
|
||||
this.set("errorMessage", response.error);
|
||||
return;
|
||||
}
|
||||
|
||||
this.setProperties({
|
||||
errorMessage: null,
|
||||
secondFactorKey: response.key,
|
||||
secondFactorImage: response.qr
|
||||
});
|
||||
})
|
||||
.catch(popupAjaxError)
|
||||
.finally(() => this.set("loading", false));
|
||||
this.markDirty();
|
||||
this.loadSecondFactors();
|
||||
this.set("password", null);
|
||||
},
|
||||
|
||||
resetPassword() {
|
||||
@ -113,16 +110,66 @@ export default Ember.Controller.extend({
|
||||
.finally(() => this.set("resetPasswordLoading", false));
|
||||
},
|
||||
|
||||
showSecondFactorKey() {
|
||||
this.set("showSecondFactorKey", true);
|
||||
disableAllSecondFactors() {
|
||||
if (this.loading) {
|
||||
return;
|
||||
}
|
||||
bootbox.confirm(
|
||||
I18n.t("user.second_factor.disable_confirm"),
|
||||
I18n.t("cancel"),
|
||||
I18n.t("user.second_factor.disable"),
|
||||
result => {
|
||||
if (result) {
|
||||
this.model
|
||||
.disableAllSecondFactors()
|
||||
.then(() => {
|
||||
const usernameLower = this.model.username.toLowerCase();
|
||||
DiscourseURL.redirectTo(
|
||||
userPath(`${usernameLower}/preferences`)
|
||||
);
|
||||
})
|
||||
.catch(e => this.handleError(e))
|
||||
.finally(() => this.set("loading", false));
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
enableSecondFactor() {
|
||||
this.toggleSecondFactor(true);
|
||||
createTotp() {
|
||||
const controller = showModal("second-factor-add-totp", {
|
||||
model: this.model,
|
||||
title: "user.second_factor.totp.add"
|
||||
});
|
||||
controller.setProperties({
|
||||
onClose: () => this.loadSecondFactors(),
|
||||
markDirty: () => this.markDirty(),
|
||||
onError: e => this.handleError(e)
|
||||
});
|
||||
},
|
||||
|
||||
disableSecondFactor() {
|
||||
this.toggleSecondFactor(false);
|
||||
editSecondFactor(second_factor) {
|
||||
const controller = showModal("second-factor-edit", {
|
||||
model: second_factor,
|
||||
title: "user.second_factor.edit_title"
|
||||
});
|
||||
controller.setProperties({
|
||||
user: this.model,
|
||||
onClose: () => this.loadSecondFactors(),
|
||||
markDirty: () => this.markDirty(),
|
||||
onError: e => this.handleError(e)
|
||||
});
|
||||
},
|
||||
|
||||
editSecondFactorBackup() {
|
||||
const controller = showModal("second-factor-backup-edit", {
|
||||
model: this.model,
|
||||
title: "user.second_factor_backup.title"
|
||||
});
|
||||
controller.setProperties({
|
||||
onClose: () => this.loadSecondFactors(),
|
||||
markDirty: () => this.markDirty(),
|
||||
onError: e => this.handleError(e)
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -0,0 +1,67 @@
|
||||
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
||||
|
||||
export default Ember.Controller.extend(ModalFunctionality, {
|
||||
loading: false,
|
||||
secondFactorImage: null,
|
||||
secondFactorKey: null,
|
||||
showSecondFactorKey: false,
|
||||
errorMessage: null,
|
||||
|
||||
onShow() {
|
||||
this.setProperties({
|
||||
errorMessage: null,
|
||||
secondFactorKey: null,
|
||||
secondFactorToken: null,
|
||||
showSecondFactorKey: false,
|
||||
secondFactorImage: null,
|
||||
loading: true
|
||||
});
|
||||
this.model
|
||||
.createSecondFactorTotp()
|
||||
.then(response => {
|
||||
if (response.error) {
|
||||
this.set("errorMessage", response.error);
|
||||
return;
|
||||
}
|
||||
|
||||
this.setProperties({
|
||||
errorMessage: null,
|
||||
secondFactorKey: response.key,
|
||||
secondFactorImage: response.qr
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
this.send("closeModal");
|
||||
this.onError(error);
|
||||
})
|
||||
.finally(() => this.set("loading", false));
|
||||
},
|
||||
|
||||
actions: {
|
||||
showSecondFactorKey() {
|
||||
this.set("showSecondFactorKey", true);
|
||||
},
|
||||
|
||||
enableSecondFactor() {
|
||||
if (!this.secondFactorToken) return;
|
||||
this.set("loading", true);
|
||||
|
||||
this.model
|
||||
.enableSecondFactorTotp(
|
||||
this.secondFactorToken,
|
||||
I18n.t("user.second_factor.totp.default_name")
|
||||
)
|
||||
.then(response => {
|
||||
if (response.error) {
|
||||
this.set("errorMessage", response.error);
|
||||
return;
|
||||
}
|
||||
this.markDirty();
|
||||
this.set("errorMessage", null);
|
||||
this.send("closeModal");
|
||||
})
|
||||
.catch(error => this.onError(error))
|
||||
.finally(() => this.set("loading", false));
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -1,9 +1,8 @@
|
||||
import { default as computed } from "ember-addons/ember-computed-decorators";
|
||||
import { default as DiscourseURL, userPath } from "discourse/lib/url";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { SECOND_FACTOR_METHODS } from "discourse/models/user";
|
||||
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
export default Ember.Controller.extend(ModalFunctionality, {
|
||||
loading: false,
|
||||
errorMessage: null,
|
||||
successMessage: null,
|
||||
@ -14,25 +13,6 @@ export default Ember.Controller.extend({
|
||||
backupCodes: null,
|
||||
secondFactorMethod: SECOND_FACTOR_METHODS.TOTP,
|
||||
|
||||
@computed("secondFactorToken", "secondFactorMethod")
|
||||
isValidSecondFactorToken(secondFactorToken, secondFactorMethod) {
|
||||
if (secondFactorMethod === SECOND_FACTOR_METHODS.TOTP) {
|
||||
return secondFactorToken && secondFactorToken.length === 6;
|
||||
} else if (secondFactorMethod === SECOND_FACTOR_METHODS.BACKUP_CODE) {
|
||||
return secondFactorToken && secondFactorToken.length === 16;
|
||||
}
|
||||
},
|
||||
|
||||
@computed("isValidSecondFactorToken", "backupEnabled", "loading")
|
||||
isDisabledGenerateBackupCodeBtn(isValid, backupEnabled, loading) {
|
||||
return !isValid || loading;
|
||||
},
|
||||
|
||||
@computed("isValidSecondFactorToken", "backupEnabled", "loading")
|
||||
isDisabledDisableBackupCodeBtn(isValid, backupEnabled, loading) {
|
||||
return !isValid || !backupEnabled || loading;
|
||||
},
|
||||
|
||||
@computed("backupEnabled")
|
||||
generateBackupCodeBtnLabel(backupEnabled) {
|
||||
return backupEnabled
|
||||
@ -40,6 +20,15 @@ export default Ember.Controller.extend({
|
||||
: "user.second_factor_backup.enable";
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.setProperties({
|
||||
loading: false,
|
||||
errorMessage: null,
|
||||
successMessage: null,
|
||||
backupCodes: null
|
||||
});
|
||||
},
|
||||
|
||||
actions: {
|
||||
copyBackupCode(successful) {
|
||||
if (successful) {
|
||||
@ -59,18 +48,10 @@ export default Ember.Controller.extend({
|
||||
|
||||
disableSecondFactorBackup() {
|
||||
this.set("backupCodes", []);
|
||||
|
||||
if (!this.secondFactorToken) return;
|
||||
|
||||
this.set("loading", true);
|
||||
|
||||
this.model
|
||||
.toggleSecondFactor(
|
||||
this.secondFactorToken,
|
||||
this.secondFactorMethod,
|
||||
SECOND_FACTOR_METHODS.BACKUP_CODE,
|
||||
false
|
||||
)
|
||||
.updateSecondFactor(0, "", true, SECOND_FACTOR_METHODS.BACKUP_CODE)
|
||||
.then(response => {
|
||||
if (response.error) {
|
||||
this.set("errorMessage", response.error);
|
||||
@ -78,28 +59,28 @@ export default Ember.Controller.extend({
|
||||
}
|
||||
|
||||
this.set("errorMessage", null);
|
||||
|
||||
const usernameLower = this.model.username.toLowerCase();
|
||||
DiscourseURL.redirectTo(userPath(`${usernameLower}/preferences`));
|
||||
this.model.set("second_factor_backup_enabled", false);
|
||||
this.markDirty();
|
||||
this.send("closeModal");
|
||||
})
|
||||
.catch(error => {
|
||||
this.send("closeModal");
|
||||
this.onError(error);
|
||||
})
|
||||
.catch(popupAjaxError)
|
||||
.finally(() => this.set("loading", false));
|
||||
},
|
||||
|
||||
generateSecondFactorCodes() {
|
||||
if (!this.secondFactorToken) return;
|
||||
this.set("loading", true);
|
||||
this.model
|
||||
.generateSecondFactorCodes(
|
||||
this.secondFactorToken,
|
||||
this.secondFactorMethod
|
||||
)
|
||||
.generateSecondFactorCodes()
|
||||
.then(response => {
|
||||
if (response.error) {
|
||||
this.set("errorMessage", response.error);
|
||||
return;
|
||||
}
|
||||
|
||||
this.markDirty();
|
||||
this.setProperties({
|
||||
errorMessage: null,
|
||||
backupCodes: response.backup_codes,
|
||||
@ -107,11 +88,13 @@ export default Ember.Controller.extend({
|
||||
remainingCodes: response.backup_codes.length
|
||||
});
|
||||
})
|
||||
.catch(popupAjaxError)
|
||||
.catch(error => {
|
||||
this.send("closeModal");
|
||||
this.onError(error);
|
||||
})
|
||||
.finally(() => {
|
||||
this.setProperties({
|
||||
loading: false,
|
||||
secondFactorToken: null
|
||||
loading: false
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -0,0 +1,53 @@
|
||||
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
||||
|
||||
export default Ember.Controller.extend(ModalFunctionality, {
|
||||
actions: {
|
||||
disableSecondFactor() {
|
||||
this.user
|
||||
.updateSecondFactor(
|
||||
this.model.id,
|
||||
this.model.name,
|
||||
true,
|
||||
this.model.method
|
||||
)
|
||||
.then(response => {
|
||||
if (response.error) {
|
||||
return;
|
||||
}
|
||||
this.markDirty();
|
||||
})
|
||||
.catch(error => {
|
||||
this.send("closeModal");
|
||||
this.onError(error);
|
||||
})
|
||||
.finally(() => {
|
||||
this.set("loading", false);
|
||||
this.send("closeModal");
|
||||
});
|
||||
},
|
||||
|
||||
editSecondFactor() {
|
||||
this.user
|
||||
.updateSecondFactor(
|
||||
this.model.id,
|
||||
this.model.name,
|
||||
false,
|
||||
this.model.method
|
||||
)
|
||||
.then(response => {
|
||||
if (response.error) {
|
||||
return;
|
||||
}
|
||||
this.markDirty();
|
||||
})
|
||||
.catch(error => {
|
||||
this.send("closeModal");
|
||||
this.onError(error);
|
||||
})
|
||||
.finally(() => {
|
||||
this.set("loading", false);
|
||||
this.send("closeModal");
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -61,7 +61,10 @@ if (Discourse.SiteSettings.tagging_enabled) {
|
||||
class: "btn-default"
|
||||
});
|
||||
}
|
||||
addBulkButton("deleteTopics", "delete", { icon: "trash", class: "btn-danger" });
|
||||
addBulkButton("deleteTopics", "delete", {
|
||||
icon: "trash-alt",
|
||||
class: "btn-danger"
|
||||
});
|
||||
|
||||
// Modal for performing bulk actions on topics
|
||||
export default Ember.Controller.extend(ModalFunctionality, {
|
||||
|
||||
@ -17,6 +17,7 @@ import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import { spinnerHTML } from "discourse/helpers/loading-spinner";
|
||||
import { userPath } from "discourse/lib/url";
|
||||
import showModal from "discourse/lib/show-modal";
|
||||
import TopicTimer from "discourse/models/topic-timer";
|
||||
|
||||
let customPostMessageCallbacks = {};
|
||||
|
||||
@ -522,6 +523,16 @@ export default Ember.Controller.extend(bufferedProperty("model"), {
|
||||
|
||||
if (user.get("staff") && hasReplies) {
|
||||
ajax(`/posts/${post.id}/reply-ids.json`).then(replies => {
|
||||
if (replies.length === 0) {
|
||||
return post
|
||||
.destroy(user)
|
||||
.then(refresh)
|
||||
.catch(error => {
|
||||
popupAjaxError(error);
|
||||
post.undoDeleteState();
|
||||
});
|
||||
}
|
||||
|
||||
const buttons = [];
|
||||
|
||||
buttons.push({
|
||||
@ -940,6 +951,46 @@ export default Ember.Controller.extend(bufferedProperty("model"), {
|
||||
}
|
||||
},
|
||||
|
||||
joinGroup() {
|
||||
const groupId = this.get("model.group.id");
|
||||
if (groupId) {
|
||||
if (this.get("model.group.allow_membership_requests")) {
|
||||
const groupName = this.get("model.group.name");
|
||||
return ajax(`/groups/${groupName}/request_membership`, {
|
||||
type: "POST",
|
||||
data: {
|
||||
topic_id: this.get("model.id")
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
bootbox.alert(
|
||||
I18n.t("topic.group_request_sent", {
|
||||
group_name: this.get("model.group.full_name")
|
||||
}),
|
||||
() =>
|
||||
this.previousURL
|
||||
? DiscourseURL.routeTo(this.previousURL)
|
||||
: DiscourseURL.routeTo("/")
|
||||
);
|
||||
})
|
||||
.catch(popupAjaxError);
|
||||
} else {
|
||||
const topic = this.model;
|
||||
return ajax(`/groups/${groupId}/members`, {
|
||||
type: "PUT",
|
||||
data: { user_id: this.get("currentUser.id") }
|
||||
})
|
||||
.then(() =>
|
||||
topic.reload().then(() => {
|
||||
topic.set("view_hidden", false);
|
||||
topic.postStream.refresh();
|
||||
})
|
||||
)
|
||||
.catch(popupAjaxError);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
replyAsNewTopic(post, quotedText) {
|
||||
const composerController = this.composer;
|
||||
|
||||
@ -1035,6 +1086,18 @@ export default Ember.Controller.extend(bufferedProperty("model"), {
|
||||
|
||||
resetBumpDate() {
|
||||
this.model.resetBumpDate();
|
||||
},
|
||||
|
||||
removeTopicTimer(statusType, topicTimer) {
|
||||
TopicTimer.updateStatus(
|
||||
this.get("model.id"),
|
||||
null,
|
||||
null,
|
||||
statusType,
|
||||
null
|
||||
)
|
||||
.then(() => this.set(`model.${topicTimer}`, Ember.Object.create({})))
|
||||
.catch(error => popupAjaxError(error));
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@ -5,9 +5,9 @@ export default {
|
||||
name: "avatar-select",
|
||||
|
||||
initialize(container) {
|
||||
this.selectAvatarsEnabled = container.lookup(
|
||||
this.selectableAvatarsEnabled = container.lookup(
|
||||
"site-settings:main"
|
||||
).select_avatars_enabled;
|
||||
).selectable_avatars_enabled;
|
||||
|
||||
container
|
||||
.lookup("app-events:main")
|
||||
@ -27,7 +27,7 @@ export default {
|
||||
const modal = showModal("avatar-selector");
|
||||
modal.setProperties({ user, selected });
|
||||
|
||||
if (this.selectAvatarsEnabled) {
|
||||
if (this.selectableAvatarsEnabled) {
|
||||
ajax("/site/selectable-avatars.json").then(avatars =>
|
||||
modal.set("selectableAvatars", avatars)
|
||||
);
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { cleanDOM } from "discourse/lib/clean-dom";
|
||||
import {
|
||||
startPageTracking,
|
||||
resetPageTracking,
|
||||
googleTagManagerPageChanged
|
||||
} from "discourse/lib/page-tracker";
|
||||
import { viewTrackingRequired } from "discourse/lib/ajax";
|
||||
@ -49,5 +50,9 @@ export default {
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
teardown() {
|
||||
resetPageTracking();
|
||||
}
|
||||
};
|
||||
|
||||
@ -101,7 +101,7 @@ const DiscourseLocation = Ember.Object.extend({
|
||||
const state = this.getState();
|
||||
path = this.formatURL(path);
|
||||
|
||||
if (state && state.path !== path) {
|
||||
if (!state || state.path !== path) {
|
||||
this.replaceState(path);
|
||||
}
|
||||
},
|
||||
|
||||
@ -12,8 +12,7 @@ Eyeline.prototype.update = function() {
|
||||
windowHeight = $(window).height(),
|
||||
docViewBottom = docViewTop + windowHeight,
|
||||
$elements = $(this.selector),
|
||||
bottomOffset = $elements.last().offset(),
|
||||
self = this;
|
||||
bottomOffset = $elements.last().offset();
|
||||
|
||||
let atBottom = false;
|
||||
if (bottomOffset) {
|
||||
@ -21,7 +20,7 @@ Eyeline.prototype.update = function() {
|
||||
bottomOffset.top <= docViewBottom && bottomOffset.top >= docViewTop;
|
||||
}
|
||||
|
||||
return $elements.each(function(i, elem) {
|
||||
return $elements.each((i, elem) => {
|
||||
const $elem = $(elem),
|
||||
elemTop = $elem.offset().top,
|
||||
elemBottom = elemTop + $elem.height();
|
||||
@ -45,17 +44,17 @@ Eyeline.prototype.update = function() {
|
||||
|
||||
// If you hit the bottom we mark all the elements as seen. Otherwise, just the first one
|
||||
if (!atBottom) {
|
||||
self.trigger("saw", { detail: $elem });
|
||||
this.trigger("saw", { detail: $elem });
|
||||
if (i === 0) {
|
||||
self.trigger("sawTop", { detail: $elem });
|
||||
this.trigger("sawTop", { detail: $elem });
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (i === 0) {
|
||||
self.trigger("sawTop", { detail: $elem });
|
||||
this.trigger("sawTop", { detail: $elem });
|
||||
}
|
||||
if (i === $elements.length - 1) {
|
||||
return self.trigger("sawBottom", { detail: $elem });
|
||||
return this.trigger("sawBottom", { detail: $elem });
|
||||
}
|
||||
});
|
||||
};
|
||||
@ -65,10 +64,8 @@ Eyeline.prototype.flushRest = function() {
|
||||
if (Ember.testing) {
|
||||
return;
|
||||
}
|
||||
const self = this;
|
||||
$(this.selector).each(function(i, elem) {
|
||||
return self.trigger("saw", { detail: $(elem) });
|
||||
});
|
||||
|
||||
$(this.selector).each((i, elem) => this.trigger("saw", { detail: $(elem) }));
|
||||
};
|
||||
|
||||
RSVP.EventTarget.mixin(Eyeline.prototype);
|
||||
|
||||
@ -65,9 +65,10 @@ const bindings = {
|
||||
"shift+p": { handler: "pinUnpinTopic" },
|
||||
"shift+r": { handler: "replyToTopic" },
|
||||
"shift+s": { click: "#topic-footer-buttons button.share", anonymous: true }, // share topic
|
||||
"shift+u": { handler: "goToUnreadPost" },
|
||||
"shift+l": { handler: "goToUnreadPost" },
|
||||
"shift+z shift+z": { handler: "logout" },
|
||||
"shift+f11": { handler: "fullscreenComposer", global: true },
|
||||
"shift+u": { handler: "deferTopic" },
|
||||
t: { postAction: "replyAsNewTopic" },
|
||||
u: { handler: "goBack", anonymous: true },
|
||||
"x r": {
|
||||
@ -438,9 +439,22 @@ export default {
|
||||
$selected = $articles.filter("[data-islastviewedtopic=true]");
|
||||
}
|
||||
|
||||
// Discard selection if it is not in viewport, so users can combine
|
||||
// keyboard shortcuts with mouse scrolling.
|
||||
if ($selected.length !== 0) {
|
||||
const offset = minimumOffset();
|
||||
const beginScreen = $(window).scrollTop() - offset;
|
||||
const endScreen = beginScreen + window.innerHeight + offset;
|
||||
const beginArticle = $selected.offset().top;
|
||||
const endArticle = $selected.offset().top + $selected.height();
|
||||
if (beginScreen > endArticle || beginArticle > endScreen) {
|
||||
$selected = null;
|
||||
}
|
||||
}
|
||||
|
||||
// If still nothing is selected, select the first post that is
|
||||
// visible and cancel move operation.
|
||||
if ($selected.length === 0) {
|
||||
if (!$selected || $selected.length === 0) {
|
||||
const offset = minimumOffset();
|
||||
$selected = $articles
|
||||
.toArray()
|
||||
@ -618,5 +632,9 @@ export default {
|
||||
|
||||
_replyToPost() {
|
||||
this.container.lookup("controller:topic").send("replyToPost");
|
||||
},
|
||||
|
||||
deferTopic() {
|
||||
this.container.lookup("controller:topic").send("deferTopic");
|
||||
}
|
||||
};
|
||||
|
||||
@ -10,9 +10,7 @@ function replaceSpan($e, username, opts) {
|
||||
|
||||
if (opts && opts.group) {
|
||||
if (opts.mentionable) {
|
||||
extra = `data-name='${username}' data-mentionable-user-count='${
|
||||
opts.mentionable.user_count
|
||||
}' data-max-mentions='${maxGroupMention}'`;
|
||||
extra = `data-name='${username}' data-mentionable-user-count='${opts.mentionable.user_count}' data-max-mentions='${maxGroupMention}'`;
|
||||
extraClass = "notify";
|
||||
}
|
||||
$e.replaceWith(
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
let _started = false;
|
||||
|
||||
const cache = {};
|
||||
let cache = {};
|
||||
let transitionCount = 0;
|
||||
|
||||
export function setTransient(key, data, count) {
|
||||
@ -11,6 +10,12 @@ export function getTransient(key) {
|
||||
return cache[key];
|
||||
}
|
||||
|
||||
export function resetPageTracking() {
|
||||
_started = false;
|
||||
transitionCount = 0;
|
||||
cache = {};
|
||||
}
|
||||
|
||||
export function startPageTracking(router, appEvents) {
|
||||
if (_started) {
|
||||
return;
|
||||
|
||||
@ -42,6 +42,7 @@ import { registerCustomPostMessageCallback as registerCustomPostMessageCallback1
|
||||
import Sharing from "discourse/lib/sharing";
|
||||
import { addComposerUploadHandler } from "discourse/components/composer-editor";
|
||||
import { addCategorySortCriteria } from "discourse/components/edit-category-settings";
|
||||
import { queryRegistry } from "discourse/widgets/widget";
|
||||
|
||||
// If you add any methods to the API ensure you bump up this number
|
||||
const PLUGIN_API_VERSION = "0.8.31";
|
||||
@ -325,7 +326,9 @@ class PluginApi {
|
||||
* ```
|
||||
**/
|
||||
attachWidgetAction(widget, actionName, fn) {
|
||||
const widgetClass = this.container.factoryFor(`widget:${widget}`).class;
|
||||
const widgetClass =
|
||||
queryRegistry(widget) ||
|
||||
this.container.factoryFor(`widget:${widget}`).class;
|
||||
widgetClass.prototype[actionName] = fn;
|
||||
}
|
||||
|
||||
|
||||
@ -53,9 +53,7 @@ export function registerTopicFooterButton(button) {
|
||||
!normalizedButton.translatedTitle
|
||||
) {
|
||||
Ember.error(
|
||||
`Attempted to register a topic button: ${
|
||||
button.id
|
||||
} with no icon or title.`
|
||||
`Attempted to register a topic button: ${button.id} with no icon or title.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -21,6 +21,7 @@ export default function(topic, params) {
|
||||
let tags = topic.tags;
|
||||
let buffer = "";
|
||||
let tagsForUser = null;
|
||||
let tagName;
|
||||
const isPrivateMessage = topic.get("isPrivateMessage");
|
||||
|
||||
if (params) {
|
||||
@ -30,6 +31,9 @@ export default function(topic, params) {
|
||||
if (params.tagsForUser) {
|
||||
tagsForUser = params.tagsForUser;
|
||||
}
|
||||
if (params.tagName) {
|
||||
tagName = params.tagName;
|
||||
}
|
||||
}
|
||||
|
||||
let customHtml = null;
|
||||
@ -50,7 +54,8 @@ export default function(topic, params) {
|
||||
buffer = "<div class='discourse-tags'>";
|
||||
if (tags) {
|
||||
for (let i = 0; i < tags.length; i++) {
|
||||
buffer += renderTag(tags[i], { isPrivateMessage, tagsForUser }) + " ";
|
||||
buffer +=
|
||||
renderTag(tags[i], { isPrivateMessage, tagsForUser, tagName }) + " ";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -116,16 +116,15 @@ function positioningWorkaround($fixedElement) {
|
||||
var blurred = debounce(blurredNow, 250);
|
||||
|
||||
var positioningHack = function(evt) {
|
||||
const self = this;
|
||||
done = false;
|
||||
|
||||
// we need this, otherwise changing focus means we never clear
|
||||
self.addEventListener("blur", blurred);
|
||||
this.addEventListener("blur", blurred);
|
||||
|
||||
if (fixedElement.style.top === "0px") {
|
||||
if (this !== document.activeElement) {
|
||||
evt.preventDefault();
|
||||
self.focus();
|
||||
this.focus();
|
||||
}
|
||||
return;
|
||||
}
|
||||
@ -157,7 +156,7 @@ function positioningWorkaround($fixedElement) {
|
||||
$(fixedElement).addClass("no-transition");
|
||||
|
||||
evt.preventDefault();
|
||||
self.focus();
|
||||
this.focus();
|
||||
workaroundActive = true;
|
||||
};
|
||||
|
||||
|
||||
@ -1,36 +1,7 @@
|
||||
import { isAppleDevice } from "discourse/lib/utilities";
|
||||
|
||||
export default function(name, opts) {
|
||||
opts = opts || {};
|
||||
const container = Discourse.__container__;
|
||||
|
||||
// iOS 11 -> 11.1 have broken INPUTs on position fixed
|
||||
// if for any reason there is a body higher than 100% behind them.
|
||||
// What happens is that when INPUTs gets focus they shift the body
|
||||
// which ends up moving the cursor to an invisible spot
|
||||
// this makes the login experience on iOS painful, user thinks it is broken.
|
||||
//
|
||||
// Also, very little value in showing main outlet and header on iOS
|
||||
// anyway, so just hide it.
|
||||
if (isAppleDevice()) {
|
||||
let pos = $(window).scrollTop();
|
||||
$(window)
|
||||
.off("show.bs.modal.ios-hacks")
|
||||
.on("show.bs.modal.ios-hacks", () => {
|
||||
$("#main-outlet, header").hide();
|
||||
});
|
||||
|
||||
$(window)
|
||||
.off("hide.bs.modal.ios-hacks")
|
||||
.on("hide.bs.modal.ios-hacks", () => {
|
||||
$("#main-outlet, header").show();
|
||||
$(window).scrollTop(pos);
|
||||
|
||||
$(window).off("hide.bs.modal.ios-hacks");
|
||||
$(window).off("show.bs.modal.ios-hacks");
|
||||
});
|
||||
}
|
||||
|
||||
// We use the container here because modals are like singletons
|
||||
// in Discourse. Only one can be shown with a particular state.
|
||||
const route = container.lookup("route:application");
|
||||
|
||||
@ -206,8 +206,16 @@ export class Tag {
|
||||
["lightbox", "d-lazyload"].includes(attr.class) &&
|
||||
hasChild(e, "img")
|
||||
) {
|
||||
let href = attr.href;
|
||||
const img = (e.children || []).find(c => c.name === "img");
|
||||
const base62SHA1 = img.attributes["data-base62-sha1"];
|
||||
text = attr.title || "";
|
||||
return "";
|
||||
|
||||
if (base62SHA1) {
|
||||
href = `upload://${base62SHA1}`;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
if (attr.href && text !== attr.href) {
|
||||
|
||||
@ -428,13 +428,6 @@ const DiscourseURL = Ember.Object.extend({
|
||||
|
||||
if (opts.replaceURL) {
|
||||
this.replaceState(path);
|
||||
} else {
|
||||
const discoveryTopics = this.controllerFor("discovery/topics");
|
||||
if (discoveryTopics) {
|
||||
discoveryTopics.resetParams();
|
||||
}
|
||||
|
||||
router._routerMicrolib.updateURL(path);
|
||||
}
|
||||
|
||||
const split = path.split("#");
|
||||
@ -445,7 +438,16 @@ const DiscourseURL = Ember.Object.extend({
|
||||
elementId = split[1];
|
||||
}
|
||||
|
||||
const transition = router.handleURL(path);
|
||||
// The default path has a hack to allow `/` to default to defaultHomepage
|
||||
// via BareRouter.handleUrl
|
||||
let transition;
|
||||
if (path === "/" || path.substring(0, 2) === "/?") {
|
||||
router._routerMicrolib.updateURL(path);
|
||||
transition = router.handleURL(path);
|
||||
} else {
|
||||
transition = router.transitionTo(path);
|
||||
}
|
||||
|
||||
transition._discourse_intercepted = true;
|
||||
const promise = transition.promise || transition;
|
||||
promise.then(() => jumpToElement(elementId));
|
||||
|
||||
@ -18,7 +18,7 @@ export default Ember.Mixin.create({
|
||||
this.selected.clear();
|
||||
},
|
||||
|
||||
dismissRead(operationType) {
|
||||
dismissRead(operationType, categoryOptions) {
|
||||
let operation;
|
||||
if (operationType === "posts") {
|
||||
operation = { type: "dismiss_posts" };
|
||||
@ -36,7 +36,8 @@ export default Ember.Mixin.create({
|
||||
promise = Discourse.Topic.bulkOperationByFilter(
|
||||
"unread",
|
||||
operation,
|
||||
this.get("category.id")
|
||||
this.get("category.id"),
|
||||
categoryOptions
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -29,7 +29,7 @@ const LoginMethod = Ember.Object.extend({
|
||||
authUrl += "?reconnect=true";
|
||||
}
|
||||
|
||||
if (fullScreenLogin) {
|
||||
if (fullScreenLogin || this.full_screen_login) {
|
||||
document.cookie = "fsl=true";
|
||||
window.location = authUrl;
|
||||
} else {
|
||||
|
||||
@ -69,7 +69,8 @@ const Post = RestModel.extend({
|
||||
return postNumber === 1 ? `${baseUrl}/1` : baseUrl;
|
||||
},
|
||||
|
||||
@computed("username") usernameUrl: userPath,
|
||||
@computed("username")
|
||||
usernameUrl: userPath,
|
||||
|
||||
topicOwner: propertyEqual("topic.details.created_by.id", "user_id"),
|
||||
|
||||
|
||||
@ -12,11 +12,11 @@ StaticPage.reopenClass({
|
||||
text = text.match(
|
||||
/<!-- preload-content: -->((?:.|[\n\r])*)<!-- :preload-content -->/
|
||||
)[1];
|
||||
resolve(StaticPage.create({ path: path, html: text }));
|
||||
resolve(StaticPage.create({ path, html: text }));
|
||||
} else {
|
||||
ajax(path + ".html", { dataType: "html" }).then(function(result) {
|
||||
resolve(StaticPage.create({ path: path, html: result }));
|
||||
});
|
||||
ajax(`/${path}.html`, { dataType: "html" }).then(result =>
|
||||
resolve(StaticPage.create({ path, html: result }))
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -481,12 +481,12 @@ const Topic = RestModel.extend({
|
||||
|
||||
// Update our attributes from a JSON result
|
||||
updateFromJson(json) {
|
||||
this.details.updateFromJson(json.details);
|
||||
|
||||
const keys = Object.keys(json);
|
||||
keys.removeObject("details");
|
||||
keys.removeObject("post_stream");
|
||||
if (!json.view_hidden) {
|
||||
this.details.updateFromJson(json.details);
|
||||
|
||||
keys.removeObjects(["details", "post_stream"]);
|
||||
}
|
||||
keys.forEach(key => this.set(key, json[key]));
|
||||
},
|
||||
|
||||
@ -733,8 +733,13 @@ Topic.reopenClass({
|
||||
});
|
||||
},
|
||||
|
||||
bulkOperationByFilter(filter, operation, categoryId) {
|
||||
const data = { filter, operation };
|
||||
bulkOperationByFilter(filter, operation, categoryId, options) {
|
||||
let data = { filter, operation };
|
||||
|
||||
if (options && options.includeSubcategories) {
|
||||
data.include_subcategories = true;
|
||||
}
|
||||
|
||||
if (categoryId) data.category_id = categoryId;
|
||||
return ajax("/topics/bulk", {
|
||||
type: "PUT",
|
||||
|
||||
@ -366,6 +366,40 @@ const User = RestModel.extend({
|
||||
});
|
||||
},
|
||||
|
||||
createSecondFactorTotp() {
|
||||
return ajax("/u/create_second_factor_totp.json", {
|
||||
type: "POST"
|
||||
});
|
||||
},
|
||||
|
||||
enableSecondFactorTotp(authToken, name) {
|
||||
return ajax("/u/enable_second_factor_totp.json", {
|
||||
data: {
|
||||
second_factor_token: authToken,
|
||||
name
|
||||
},
|
||||
type: "POST"
|
||||
});
|
||||
},
|
||||
|
||||
disableAllSecondFactors() {
|
||||
return ajax("/u/disable_second_factor.json", {
|
||||
type: "PUT"
|
||||
});
|
||||
},
|
||||
|
||||
updateSecondFactor(id, name, disable, targetMethod) {
|
||||
return ajax("/u/second_factor.json", {
|
||||
data: {
|
||||
second_factor_target: targetMethod,
|
||||
name,
|
||||
disable,
|
||||
id
|
||||
},
|
||||
type: "PUT"
|
||||
});
|
||||
},
|
||||
|
||||
toggleSecondFactor(authToken, authMethod, targetMethod, enable) {
|
||||
return ajax("/u/second_factor.json", {
|
||||
data: {
|
||||
@ -378,12 +412,8 @@ const User = RestModel.extend({
|
||||
});
|
||||
},
|
||||
|
||||
generateSecondFactorCodes(authToken, authMethod) {
|
||||
generateSecondFactorCodes() {
|
||||
return ajax("/u/second_factors_backup.json", {
|
||||
data: {
|
||||
second_factor_token: authToken,
|
||||
second_factor_method: authMethod
|
||||
},
|
||||
type: "PUT"
|
||||
});
|
||||
},
|
||||
|
||||
@ -68,10 +68,12 @@ const ApplicationRoute = Discourse.Route.extend(OpenComposer, {
|
||||
composePrivateMessage(user, post) {
|
||||
const recipient = user ? user.get("username") : "",
|
||||
reply = post
|
||||
? window.location.protocol +
|
||||
"//" +
|
||||
window.location.host +
|
||||
post.get("url")
|
||||
? `${window.location.protocol}//${window.location.host}${post.url}`
|
||||
: null,
|
||||
title = post
|
||||
? I18n.t("composer.reference_topic_title", {
|
||||
title: post.topic.title
|
||||
})
|
||||
: null;
|
||||
|
||||
// used only once, one less dependency
|
||||
@ -80,7 +82,8 @@ const ApplicationRoute = Discourse.Route.extend(OpenComposer, {
|
||||
usernames: recipient,
|
||||
archetypeId: "private_message",
|
||||
draftKey: "new_private_message",
|
||||
reply: reply
|
||||
reply,
|
||||
title
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@ -60,12 +60,15 @@ export default Discourse.Route.extend(OpenComposer, {
|
||||
},
|
||||
|
||||
dismissReadTopics(dismissTopics) {
|
||||
var operationType = dismissTopics ? "topics" : "posts";
|
||||
this.controllerFor("discovery/topics").send("dismissRead", operationType);
|
||||
const operationType = dismissTopics ? "topics" : "posts";
|
||||
this.send("dismissRead", operationType);
|
||||
},
|
||||
|
||||
dismissRead(operationType) {
|
||||
this.controllerFor("discovery/topics").send("dismissRead", operationType);
|
||||
const controller = this.controllerFor("discovery/topics");
|
||||
controller.send("dismissRead", operationType, {
|
||||
includeSubcategories: !controller.noSubcategories
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,21 +0,0 @@
|
||||
import RestrictedUserRoute from "discourse/routes/restricted-user";
|
||||
|
||||
export default RestrictedUserRoute.extend({
|
||||
showFooter: true,
|
||||
|
||||
model() {
|
||||
return this.modelFor("user");
|
||||
},
|
||||
|
||||
renderTemplate() {
|
||||
return this.render({ into: "user" });
|
||||
},
|
||||
|
||||
setupController(controller, model) {
|
||||
controller.setProperties({ model, newUsername: model.get("username") });
|
||||
},
|
||||
|
||||
deactivate() {
|
||||
this.controller.setProperties({ backupCodes: null });
|
||||
}
|
||||
});
|
||||
@ -13,6 +13,23 @@ export default RestrictedUserRoute.extend({
|
||||
|
||||
setupController(controller, model) {
|
||||
controller.setProperties({ model, newUsername: model.get("username") });
|
||||
controller.set("loading", true);
|
||||
model
|
||||
.loadSecondFactorCodes("")
|
||||
.then(response => {
|
||||
if (response.error) {
|
||||
controller.set("errorMessage", response.error);
|
||||
} else {
|
||||
controller.setProperties({
|
||||
errorMessage: null,
|
||||
loaded: !response.password_required,
|
||||
dirty: !!response.password_required,
|
||||
totps: response.totps
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(controller.popupAjaxError)
|
||||
.finally(() => controller.set("loading", false));
|
||||
},
|
||||
|
||||
actions: {
|
||||
@ -26,6 +43,7 @@ export default RestrictedUserRoute.extend({
|
||||
if (
|
||||
transition.targetName === "preferences.second-factor" ||
|
||||
!user ||
|
||||
(settings.allow_anonymous_posting && user.is_anonymous) ||
|
||||
user.second_factor_enabled ||
|
||||
(settings.enforce_second_factor === "staff" && !user.staff) ||
|
||||
settings.enforce_second_factor === "no"
|
||||
|
||||
@ -1,15 +1,16 @@
|
||||
import Composer from "discourse/models/composer";
|
||||
import showModal from "discourse/lib/show-modal";
|
||||
import { findTopicList } from "discourse/routes/build-topic-route";
|
||||
import {
|
||||
filterQueryParams,
|
||||
findTopicList
|
||||
} from "discourse/routes/build-topic-route";
|
||||
import { queryParams } from "discourse/controllers/discovery-sortable";
|
||||
import PermissionType from "discourse/models/permission-type";
|
||||
|
||||
export default Discourse.Route.extend({
|
||||
navMode: "latest",
|
||||
|
||||
queryParams: {
|
||||
ascending: { refreshModel: true },
|
||||
order: { refreshModel: true }
|
||||
},
|
||||
queryParams,
|
||||
|
||||
renderTemplate() {
|
||||
const controller = this.controllerFor("tags.show");
|
||||
@ -69,10 +70,7 @@ export default Discourse.Route.extend({
|
||||
const controller = this.controllerFor("tags.show");
|
||||
controller.set("loading", true);
|
||||
|
||||
const params = controller.getProperties("order", "ascending");
|
||||
params.order = transition.to.queryParams.order || params.order;
|
||||
params.ascending = transition.to.queryParams.ascending || params.ascending;
|
||||
|
||||
const params = filterQueryParams(transition.to.queryParams, {});
|
||||
const categorySlug = this.categorySlug;
|
||||
const parentCategorySlug = this.parentCategorySlug;
|
||||
const filter = this.navMode;
|
||||
|
||||
@ -35,6 +35,11 @@ export default Discourse.Route.extend({
|
||||
// TODO we are seeing errors where closest post is null and this is exploding
|
||||
// we need better handling and logging for this condition.
|
||||
|
||||
// there are no closestPost for hidden topics
|
||||
if (topic.view_hidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The post we requested might not exist. Let's find the closest post
|
||||
const closestPost = postStream.closestPostForPostNumber(
|
||||
params.nearPost || 1
|
||||
@ -76,5 +81,18 @@ export default Discourse.Route.extend({
|
||||
console.log("Could not view topic", e);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
actions: {
|
||||
willTransition() {
|
||||
this.controllerFor("topic").set(
|
||||
"previousURL",
|
||||
document.location.pathname
|
||||
);
|
||||
|
||||
// NOTE: omitting this return can break the back button when transitioning quickly between
|
||||
// topics and the latest page.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -225,9 +225,13 @@ const TopicRoute = Discourse.Route.extend({
|
||||
|
||||
model(params, transition) {
|
||||
if (params.slug.match(ID_CONSTRAINT)) {
|
||||
return DiscourseURL.routeTo(`/t/topic/${params.slug}/${params.id}`, {
|
||||
transition.abort();
|
||||
|
||||
DiscourseURL.routeTo(`/t/topic/${params.slug}/${params.id}`, {
|
||||
replaceURL: true
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const queryParams = transition.to.queryParams;
|
||||
|
||||
@ -47,7 +47,7 @@
|
||||
class="d-editor-input"
|
||||
placeholder=placeholderTranslated
|
||||
disabled=disabled
|
||||
change=change}}
|
||||
input=change}}
|
||||
{{popup-input-tip validation=validation}}
|
||||
{{plugin-outlet name="after-d-editor" tagName="" args=outletArgs}}
|
||||
</div>
|
||||
|
||||
@ -9,6 +9,10 @@
|
||||
content=visibilityLevelOptions
|
||||
castInteger=true
|
||||
class="groups-form-visibility-level"}}
|
||||
|
||||
<div class="control-instructions">
|
||||
{{i18n 'admin.groups.manage.interaction.visibility_levels.description'}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
{{accessViaGroupText}}
|
||||
{{d-button action=action
|
||||
class="btn-primary topic-join-group"
|
||||
icon="user-plus"
|
||||
label=accessViaGroupButtonText}}
|
||||
@ -125,6 +125,12 @@
|
||||
{{#if searchActive}}
|
||||
<h3>{{i18n "search.no_results"}}</h3>
|
||||
|
||||
{{#if model.grouped_search_result.error}}
|
||||
<div class="warning">
|
||||
{{model.grouped_search_result.error}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if showSuggestion}}
|
||||
<div class="no-results-suggestion">
|
||||
{{i18n "search.cant_find"}}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<div class='bulk-buttons'>
|
||||
{{#each buttons as |button|}}
|
||||
{{d-button action=button.action label=button.label icon=button.icon class=button.class}}
|
||||
{{d-button action=(action button.action) label=button.label icon=button.icon class=button.class}}
|
||||
{{/each}}
|
||||
</div>
|
||||
|
||||
@ -8,13 +8,21 @@
|
||||
</p>
|
||||
|
||||
<form>
|
||||
{{date-picker-past value=date containerId="date-container"}}
|
||||
{{date-picker-past
|
||||
value=(unbound date)
|
||||
containerId="date-container"
|
||||
onSelect=(action (mut date))}}
|
||||
|
||||
{{input type="time" value=time}}
|
||||
</form>
|
||||
|
||||
<div id="date-container" />
|
||||
<div id="date-container"></div>
|
||||
{{/d-modal-body}}
|
||||
|
||||
<div class="modal-footer change-timestamp-footer">
|
||||
<button class="btn btn-primary" disabled={{buttonDisabled}} {{action "changeTimestamp"}}>{{buttonTitle}}</button>
|
||||
{{d-button
|
||||
class="btn-primary"
|
||||
disabled=buttonDisabled
|
||||
action=(action "changeTimestamp")
|
||||
translatedLabel=buttonTitle}}
|
||||
</div>
|
||||
|
||||
@ -23,6 +23,7 @@
|
||||
<li>{{{shortcuts.navigation.up_down}}}</li>
|
||||
<li>{{{shortcuts.navigation.open}}}</li>
|
||||
<li>{{{shortcuts.navigation.next_prev}}}</li>
|
||||
<li>{{{shortcuts.navigation.go_to_unread_post}}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
@ -64,6 +65,7 @@
|
||||
<li>{{{shortcuts.actions.mark_regular}}}</li>
|
||||
<li>{{{shortcuts.actions.mark_tracking}}}</li>
|
||||
<li>{{{shortcuts.actions.mark_watching}}}</li>
|
||||
<li>{{{shortcuts.actions.defer}}}</li>
|
||||
<li>{{{shortcuts.actions.print}}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@ -0,0 +1,51 @@
|
||||
{{#d-modal-body}}
|
||||
{{#conditional-loading-spinner condition=loading}}
|
||||
{{#if errorMessage}}
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<div class='alert alert-error'>{{errorMessage}}</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
{{{i18n 'user.second_factor.enable_description'}}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<div class="qr-code-container">
|
||||
<div class="qr-code">
|
||||
{{{secondFactorImage}}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
{{#if showSecondFactorKey}}
|
||||
{{secondFactorKey}}
|
||||
{{else}}
|
||||
<a {{action "showSecondFactorKey"}}>{{i18n 'user.second_factor.show_key_description'}}</a>
|
||||
{{/if}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label class="control-label input-prepend">{{i18n 'user.second_factor.label'}}</label>
|
||||
|
||||
<div class="controls">
|
||||
{{second-factor-input maxlength=6 value=secondFactorToken inputId='second-factor-token'}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
{{d-button action=(action "enableSecondFactor")
|
||||
class="btn btn-primary add-totp"
|
||||
label="enable"}}
|
||||
</div>
|
||||
</div>
|
||||
{{/conditional-loading-spinner}}
|
||||
{{/d-modal-body}}
|
||||
@ -0,0 +1,50 @@
|
||||
{{#d-modal-body}}
|
||||
<section class="user-preferences solo-preference second-factor-backup-preferences">
|
||||
<form class="form-horizontal">
|
||||
{{#if successMessage}}
|
||||
<div class="alert alert-success">
|
||||
{{successMessage}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if errorMessage}}
|
||||
<div class="alert alert-error">
|
||||
{{errorMessage}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if backupEnabled}}
|
||||
{{{i18n "user.second_factor_backup.remaining_codes" count=remainingCodes}}}
|
||||
{{/if}}
|
||||
|
||||
<div class="actions">
|
||||
{{d-button
|
||||
action=(action "generateSecondFactorCodes")
|
||||
class="btn btn-primary"
|
||||
disabled=loading
|
||||
label=generateBackupCodeBtnLabel}}
|
||||
{{#if backupEnabled}}
|
||||
{{d-button
|
||||
action=(action "disableSecondFactorBackup")
|
||||
class="btn btn-danger"
|
||||
disabled=loading
|
||||
label="user.second_factor_backup.disable"}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{#conditional-loading-section isLoading=loading}}
|
||||
{{#if backupCodes}}
|
||||
<h3>{{i18n "user.second_factor_backup.codes.title"}}</h3>
|
||||
|
||||
<p>
|
||||
{{i18n "user.second_factor_backup.codes.description"}}
|
||||
</p>
|
||||
|
||||
{{backup-codes
|
||||
copyBackupCode=(action "copyBackupCode")
|
||||
backupCodes=backupCodes}}
|
||||
{{/if}}
|
||||
{{/conditional-loading-section}}
|
||||
</form>
|
||||
</section>
|
||||
{{/d-modal-body}}
|
||||
@ -0,0 +1,15 @@
|
||||
{{#d-modal-body}}
|
||||
<div class="form-horizontal">
|
||||
{{input type="text" value=model.name}}
|
||||
</div>
|
||||
<div class='second-factor instructions'>
|
||||
{{i18n 'user.second_factor.edit_description'}}
|
||||
</div>
|
||||
{{d-button action=(action "editSecondFactor")
|
||||
class="btn-primary"
|
||||
label="user.second_factor.edit"}}
|
||||
|
||||
{{d-button action=(action "disableSecondFactor")
|
||||
class="btn-danger"
|
||||
label="user.second_factor.disable"}}
|
||||
{{/d-modal-body}}
|
||||
@ -1,71 +0,0 @@
|
||||
<section class="user-preferences solo-preference second-factor-backup-preferences">
|
||||
<form class="form-horizontal">
|
||||
<div class="control-group">
|
||||
|
||||
<div class="controls">
|
||||
{{#if successMessage}}
|
||||
<div class="alert alert-success">
|
||||
{{successMessage}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if errorMessage}}
|
||||
<div class="alert alert-error">
|
||||
{{errorMessage}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
{{#second-factor-form
|
||||
secondFactorMethod=secondFactorMethod
|
||||
backupEnabled=backupEnabled
|
||||
secondFactorToken=secondFactorToken
|
||||
secondFactorTitle=(i18n 'user.second_factor_backup.title')
|
||||
optionalText=(if backupEnabled (i18n "user.second_factor_backup.remaining_codes" count=remainingCodes))
|
||||
isLogin=false}}
|
||||
{{second-factor-input value=secondFactorToken inputId='second-factor-token' secondFactorMethod=secondFactorMethod}}
|
||||
{{/second-factor-form}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<div class="actions">
|
||||
{{d-button
|
||||
action=(action "generateSecondFactorCodes")
|
||||
class="btn btn-primary"
|
||||
disabled=isDisabledGenerateBackupCodeBtn
|
||||
label=generateBackupCodeBtnLabel}}
|
||||
{{#if backupEnabled}}
|
||||
{{d-button
|
||||
action=(action "disableSecondFactorBackup")
|
||||
class="btn btn-danger"
|
||||
disabled=isDisabledDisableBackupCodeBtn
|
||||
label="user.second_factor_backup.disable"}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{#conditional-loading-section isLoading=loading}}
|
||||
{{#if backupCodes}}
|
||||
<h3>{{i18n "user.second_factor_backup.codes.title"}}</h3>
|
||||
|
||||
<p>
|
||||
{{i18n "user.second_factor_backup.codes.description"}}
|
||||
</p>
|
||||
|
||||
{{backup-codes
|
||||
copyBackupCode=(action "copyBackupCode")
|
||||
backupCodes=backupCodes}}
|
||||
|
||||
{{#link-to "preferences.account" model.username}}
|
||||
{{i18n "go_back"}}
|
||||
{{/link-to}}
|
||||
{{/if}}
|
||||
{{/conditional-loading-section}}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
@ -1,4 +1,5 @@
|
||||
<section class='user-preferences solo-preference'>
|
||||
<section class='user-preferences solo-preference second-factor'>
|
||||
{{#conditional-loading-spinner condition=loading}}
|
||||
<form class="form-horizontal">
|
||||
|
||||
{{#if showEnforcedNotice}}
|
||||
@ -9,6 +10,14 @@
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if displayOAuthWarning}}
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
{{i18n 'user.second_factor.oauth_enabled_warning'}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if errorMessage}}
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
@ -17,121 +26,105 @@
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if model.second_factor_enabled}}
|
||||
{{#if loaded}}
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
{{#second-factor-form
|
||||
secondFactorMethod=secondFactorMethod
|
||||
backupEnabled=backupEnabled
|
||||
secondFactorToken=secondFactorToken
|
||||
secondFactorTitle=(i18n 'user.second_factor.title')
|
||||
isLogin=false}}
|
||||
{{second-factor-input value=secondFactorToken inputId='second-factor-token' secondFactorMethod=secondFactorMethod}}
|
||||
{{/second-factor-form}}
|
||||
<h2>{{i18n "user.second_factor.totp.title"}}</h2>
|
||||
{{d-button action=(action "createTotp")
|
||||
class="btn-primary new-totp"
|
||||
disabled=loading
|
||||
label="user.second_factor.totp.add"}}
|
||||
{{#each totps as |totp|}}
|
||||
<div class="second-factor-item">
|
||||
{{totp.name}}
|
||||
|
||||
{{#if isCurrentUser}}
|
||||
{{d-button action=(action "editSecondFactor" totp)
|
||||
class="btn-default btn-small btn-icon pad-left no-text edit"
|
||||
disabled=loading
|
||||
icon="pencil-alt"
|
||||
}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<div class="controls pref-second-factor-backup">
|
||||
<h2>{{i18n "user.second_factor_backup.title"}}</h2>
|
||||
{{#if model.second_factor_enabled}}
|
||||
{{#if model.second_factor_backup_enabled}}
|
||||
{{{i18n 'user.second_factor_backup.manage' count=model.second_factor_remaining_backup_codes}}}
|
||||
{{else}}
|
||||
{{i18n 'user.second_factor_backup.enable_long'}}
|
||||
{{/if}}
|
||||
|
||||
{{#if isCurrentUser}}
|
||||
{{d-button action=(action "editSecondFactorBackup")
|
||||
class="btn-default btn-small btn-icon pad-left no-text edit edit-2fa-backup"
|
||||
disabled=loading
|
||||
icon="pencil-alt"
|
||||
}}
|
||||
{{/if}}
|
||||
{{else}}
|
||||
{{i18n "user.second_factor_backup.enable_prerequisites"}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#if model.second_factor_enabled}}
|
||||
{{#unless showEnforcedNotice}}
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<h2>{{i18n "user.second_factor.disable_title"}}</h2>
|
||||
{{d-button action=(action "disableAllSecondFactors")
|
||||
class="btn btn-danger"
|
||||
disabled=loading
|
||||
label="disable"}}
|
||||
</div>
|
||||
</div>
|
||||
{{/unless}}
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<div class="control-group">
|
||||
<label class='control-label'>{{i18n 'user.password.title'}}</label>
|
||||
|
||||
<div class="controls">
|
||||
<div>
|
||||
{{text-field value=password
|
||||
id="password"
|
||||
type="password"
|
||||
classNames="input-xxlarge"
|
||||
autofocus="autofocus"}}
|
||||
</div>
|
||||
<div class='instructions'>
|
||||
{{i18n 'user.second_factor.confirm_password_description'}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
{{d-button action=(action "disableSecondFactor")
|
||||
class="btn btn-primary"
|
||||
disabled=loading
|
||||
label=disableButtonText}}
|
||||
{{d-button action=(action "confirmPassword")
|
||||
class="btn-primary"
|
||||
disabled=loading
|
||||
label="continue"}}
|
||||
|
||||
{{d-button action=(action "resetPassword")
|
||||
class="btn"
|
||||
disabled=resetPasswordLoading
|
||||
icon="envelope"
|
||||
label='user.change_password.action'}}
|
||||
|
||||
{{resetPasswordProgress}}
|
||||
|
||||
{{#unless showEnforcedNotice}}
|
||||
{{cancel-link route="preferences.account" args= model.username}}
|
||||
{{/unless}}
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
{{#if loaded}}
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
{{{i18n 'user.second_factor.enable_description'}}}
|
||||
|
||||
{{#if displayOAuthWarning}}
|
||||
{{i18n 'user.second_factor.oauth_enabled_warning'}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
<div class="qr-code-container">
|
||||
<div class="qr-code">
|
||||
{{{secondFactorImage}}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
{{#if showSecondFactorKey}}
|
||||
{{secondFactorKey}}
|
||||
{{else}}
|
||||
<a {{action "showSecondFactorKey"}}>{{i18n 'user.second_factor.show_key_description'}}</a>
|
||||
{{/if}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label class="control-label input-prepend">{{i18n 'user.second_factor.label'}}</label>
|
||||
|
||||
<div class="controls">
|
||||
{{second-factor-input maxlength=6 value=secondFactorToken inputId='second-factor-token'}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
{{d-button action=(action "enableSecondFactor")
|
||||
class="btn btn-primary"
|
||||
disabled=loading
|
||||
label=enableButtonText}}
|
||||
|
||||
{{#unless showEnforcedNotice}}
|
||||
{{cancel-link route="preferences.account" args= model.username}}
|
||||
{{/unless}}
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="control-group">
|
||||
<label class='control-label'>{{i18n 'user.password.title'}}</label>
|
||||
|
||||
<div class="controls">
|
||||
<div>
|
||||
{{text-field value=password
|
||||
id="password"
|
||||
type="password"
|
||||
classNames="input-xxlarge"
|
||||
autofocus="autofocus"}}
|
||||
</div>
|
||||
<div class='instructions'>
|
||||
{{i18n 'user.second_factor.confirm_password_description'}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<div class="controls">
|
||||
{{d-button action=(action "confirmPassword")
|
||||
class="btn btn-primary"
|
||||
disabled=loading
|
||||
label=submitButtonText}}
|
||||
|
||||
{{d-button action=(action "resetPassword")
|
||||
class="btn"
|
||||
disabled=resetPasswordLoading
|
||||
icon="envelope"
|
||||
label='user.change_password.action'}}
|
||||
|
||||
{{resetPasswordProgress}}
|
||||
|
||||
{{#unless showEnforcedNotice}}
|
||||
{{cancel-link route="preferences.account" args= model.username}}
|
||||
{{/unless}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</form>
|
||||
{{/conditional-loading-spinner}}
|
||||
</section>
|
||||
|
||||
@ -90,32 +90,11 @@
|
||||
</label>
|
||||
{{/unless}}
|
||||
<div class="controls pref-second-factor">
|
||||
{{i18n 'user.second_factor.enable'}}
|
||||
{{#if isCurrentUser}}
|
||||
{{#if model.second_factor_enabled}}
|
||||
{{#link-to "preferences.second-factor" class="btn btn-default"}}
|
||||
{{d-icon "unlock"}} <span>{{i18n 'user.second_factor.disable'}}</span>
|
||||
{{/link-to}}
|
||||
{{else}}
|
||||
{{#link-to "preferences.second-factor" class="btn btn-default"}}
|
||||
{{d-icon "lock"}} <span>{{i18n 'user.second_factor.enable'}}</span>
|
||||
{{/link-to}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<div class="controls pref-second-factor-backup">
|
||||
{{#if model.second_factor_enabled}}
|
||||
{{#if isCurrentUser}}
|
||||
{{#link-to "preferences.second-factor-backup"}}
|
||||
<span>
|
||||
{{#if model.second_factor_backup_enabled}}
|
||||
{{i18n 'user.second_factor_backup.manage'}}
|
||||
{{else}}
|
||||
{{i18n 'user.second_factor_backup.enable_long'}}
|
||||
{{/if}}
|
||||
</span>
|
||||
{{/link-to}}
|
||||
{{/if}}
|
||||
{{#link-to "preferences.second-factor" class="btn btn-default"}}
|
||||
{{d-icon "lock"}} <span>{{i18n 'user.second_factor.enable'}}</span>
|
||||
{{/link-to}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
@ -126,31 +105,32 @@
|
||||
<label class="control-label">{{i18n 'user.associated_accounts.title'}}</label>
|
||||
{{#if associatedAccountsLoaded}}
|
||||
<table>
|
||||
{{#each authProviders as |authProvider|}}
|
||||
<tr>
|
||||
<td>{{authProvider.method.prettyName}}</td>
|
||||
|
||||
{{#each authProviders as |authProvider|}}
|
||||
{{#if authProvider.account}}
|
||||
<td>{{authProvider.account.description}}</td>
|
||||
<td>
|
||||
{{#if authProvider.method.can_revoke}}
|
||||
{{#conditional-loading-spinner condition=revoking size='small'}}
|
||||
{{d-button action=(action "revokeAccount") actionParam=authProvider.account title="user.associated_accounts.revoke" class="btn-danger no-text" icon="trash-alt" }}
|
||||
{{/conditional-loading-spinner}}
|
||||
{{/if}}
|
||||
</td>
|
||||
<tr class="{{dasherize authProvider.method.name}} account-connected">
|
||||
<td>{{authProvider.method.prettyName}}</td>
|
||||
<td>{{authProvider.account.description}}</td>
|
||||
<td>
|
||||
{{#if authProvider.method.can_revoke}}
|
||||
{{#conditional-loading-spinner condition=revoking size='small'}}
|
||||
{{d-button action=(action "revokeAccount") actionParam=authProvider.account title="user.associated_accounts.revoke" class="btn-danger no-text" icon="trash-alt" }}
|
||||
{{/conditional-loading-spinner}}
|
||||
{{/if}}
|
||||
</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<td colspan=2>
|
||||
{{#if authProvider.method.can_connect}}
|
||||
{{d-button action=(action "connectAccount") actionParam=authProvider.method label="user.associated_accounts.connect" class="btn-default" icon="plug" disabled=disableConnectButtons}}
|
||||
{{else}}
|
||||
{{i18n 'user.associated_accounts.not_connected'}}
|
||||
{{/if}}
|
||||
</td>
|
||||
<tr class="{{dasherize authProvider.method.name}}">
|
||||
<td>{{authProvider.method.prettyName}}</td>
|
||||
<td colspan=2>
|
||||
{{#if authProvider.method.can_connect}}
|
||||
{{d-button action=(action "connectAccount") actionParam=authProvider.method label="user.associated_accounts.connect" class="btn-default" icon="plug" disabled=disableConnectButtons}}
|
||||
{{else}}
|
||||
{{i18n 'user.associated_accounts.not_connected'}}
|
||||
{{/if}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/if}}
|
||||
|
||||
</tr>
|
||||
{{/each}}
|
||||
{{/each}}
|
||||
</table>
|
||||
{{else}}
|
||||
<div class="controls">
|
||||
|
||||
@ -3,25 +3,23 @@
|
||||
|
||||
<div class="controls tracking-controls">
|
||||
<label>{{d-icon "d-watching"}} {{i18n 'user.watched_categories'}}</label>
|
||||
{{#if canSee}}
|
||||
<a class="show-tracking" href="{{unbound model.watchingTopicsPath}}">{{i18n 'user.tracked_topics_link'}}</a>
|
||||
{{/if}}
|
||||
{{category-selector categories=model.watchedCategories blacklist=selectedCategories}}
|
||||
</div>
|
||||
<div class="instructions">{{i18n 'user.watched_categories_instructions'}}</div>
|
||||
{{#if canSee}}
|
||||
<div class="controls">
|
||||
<a href="{{unbound model.watchingTopicsPath}}">{{i18n 'user.watched_topics_link'}}</a>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
|
||||
<div class="controls tracking-controls">
|
||||
<label>{{d-icon "d-tracking"}} {{i18n 'user.tracked_categories'}}</label>
|
||||
{{#if canSee}}
|
||||
<a class="show-tracking" href="{{unbound model.trackingTopicsPath}}">{{i18n 'user.tracked_topics_link'}}</a>
|
||||
{{/if}}
|
||||
{{category-selector categories=model.trackedCategories blacklist=selectedCategories}}
|
||||
</div>
|
||||
<div class="instructions">{{i18n 'user.tracked_categories_instructions'}}</div>
|
||||
{{#if canSee}}
|
||||
<div class="controls">
|
||||
<a href="{{unbound model.trackingTopicsPath}}">{{i18n 'user.tracked_topics_link'}}</a>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
|
||||
<div class="controls tracking-controls">
|
||||
<label>{{d-icon "d-watching-first"}} {{i18n 'user.watched_first_post_categories'}}</label>
|
||||
@ -31,14 +29,12 @@
|
||||
|
||||
<div class="controls tracking-controls">
|
||||
<label>{{d-icon "d-muted"}} {{i18n 'user.muted_categories'}}</label>
|
||||
{{#if canSee}}
|
||||
<a class="show-tracking" href="{{unbound model.mutedTopicsPath}}">{{i18n 'user.tracked_topics_link'}}</a>
|
||||
{{/if}}
|
||||
{{category-selector categories=model.mutedCategories blacklist=selectedCategories}}
|
||||
</div>
|
||||
<div class="instructions">{{i18n (if hideMutedTags 'user.muted_categories_instructions' 'user.muted_categories_instructions_dont_hide')}}</div>
|
||||
{{#if canSee}}
|
||||
<div class="controls">
|
||||
<a href="{{unbound model.mutedTopicsPath}}">{{i18n 'user.muted_topics_link'}}</a>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{plugin-outlet name="user-preferences-categories" args=(hash model=model save=(action "save"))}}
|
||||
|
||||
@ -10,7 +10,6 @@
|
||||
<div class="instructions">{{i18n 'user.muted_users_instructions'}}</div>
|
||||
</div>
|
||||
|
||||
{{plugin-outlet name="user-preferences-notifications" args=(hash model=model save=(action "save"))}}
|
||||
{{plugin-outlet name="user-custom-controls" args=(hash model=model)}}
|
||||
|
||||
<div class="control-group save-button">
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
tags=model.tag_names
|
||||
everyTag=true
|
||||
allowCreate=true
|
||||
filterPlaceholder="tagging.groups.tags_placeholder"
|
||||
unlimitedTagCount=true}}
|
||||
</section>
|
||||
|
||||
|
||||
@ -52,32 +52,32 @@
|
||||
<div class="row">
|
||||
<div class="full-width">
|
||||
<div id='list-area'>
|
||||
{{conditional-loading-spinner condition=loading}}
|
||||
{{#unless loading}}
|
||||
{{#if list.topics}}
|
||||
{{#discovery-topics-list model=list refresh=(action "refresh")}}
|
||||
{{bulk-select-button selected=selected action=(action "refresh")}}
|
||||
|
||||
{{#unless loading}}
|
||||
{{#if list.topics}}
|
||||
{{#discovery-topics-list model=list refresh=(action "refresh")}}
|
||||
{{bulk-select-button selected=selected action=(action "refresh")}}
|
||||
{{topic-list topics=list.topics
|
||||
canBulkSelect=canBulkSelect
|
||||
toggleBulkSelect=(action "toggleBulkSelect")
|
||||
bulkSelectEnabled=bulkSelectEnabled
|
||||
selected=selected
|
||||
showPosters=true
|
||||
order=order
|
||||
ascending=ascending
|
||||
changeSort=(action "changeSort")}}
|
||||
|
||||
{{topic-list topics=list.topics
|
||||
canBulkSelect=canBulkSelect
|
||||
toggleBulkSelect=(action "toggleBulkSelect")
|
||||
bulkSelectEnabled=bulkSelectEnabled
|
||||
selected=selected
|
||||
showPosters=true
|
||||
order=order
|
||||
ascending=ascending
|
||||
changeSort=(action "changeSort")}}
|
||||
{{/discovery-topics-list}}
|
||||
{{else}}
|
||||
<footer class='topic-list-bottom'>
|
||||
<h3>
|
||||
{{footerMessage}}{{#link-to "discovery.categories"}} {{i18n 'topic.browse_all_categories'}}{{/link-to}} {{i18n 'or'}} {{#link-to 'discovery.latest'}}{{i18n 'topic.view_latest_topics'}}{{/link-to}}.
|
||||
</h3>
|
||||
</footer>
|
||||
{{/if}}
|
||||
{{/unless}}
|
||||
|
||||
{{/discovery-topics-list}}
|
||||
{{else}}
|
||||
<footer class='topic-list-bottom'>
|
||||
<h3>
|
||||
{{footerMessage}}{{#link-to "discovery.categories"}} {{i18n 'topic.browse_all_categories'}}{{/link-to}} {{i18n 'or'}} {{#link-to 'discovery.latest'}}{{i18n 'topic.view_latest_topics'}}{{/link-to}}.
|
||||
</h3>
|
||||
</footer>
|
||||
{{/if}}
|
||||
{{/unless}}
|
||||
{{conditional-loading-spinner condition=list.loadingMore}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,144 +1,99 @@
|
||||
{{#discourse-topic multiSelect=multiSelect enteredAt=enteredAt topic=model hasScrolled=hasScrolled}}
|
||||
{{#if model}}
|
||||
{{add-category-tag-classes category=model.category tags=model.tags}}
|
||||
<div class="container">
|
||||
{{discourse-banner user=currentUser banner=site.banner overlay=hasScrolled hide=model.errorLoading}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if showSharedDraftControls}}
|
||||
{{shared-draft-controls topic=model}}
|
||||
{{/if}}
|
||||
|
||||
{{plugin-outlet name="topic-above-post-stream" args=(hash model=model)}}
|
||||
|
||||
{{#if model.postStream.loaded}}
|
||||
{{#if model.postStream.firstPostPresent}}
|
||||
{{#topic-title cancelled=(action "cancelEditingTopic") save=(action "finishedEditingTopic") model=model}}
|
||||
{{#if editingTopic}}
|
||||
<div class="edit-topic-title">
|
||||
{{#if model.isPrivateMessage}}
|
||||
<span class="private-message-glyph">{{d-icon "envelope"}}</span>
|
||||
{{/if}}
|
||||
{{text-field id="edit-title" value=buffered.title maxlength=siteSettings.max_topic_title_length autofocus="true"}}
|
||||
{{#if showCategoryChooser}}
|
||||
{{category-chooser
|
||||
class="small"
|
||||
value=(unbound buffered.category_id)
|
||||
onSelectAny=(action "topicCategoryChanged")}}
|
||||
{{/if}}
|
||||
|
||||
{{#if canEditTags}}
|
||||
{{mini-tag-chooser filterable=true tags=buffered.tags categoryId=buffered.category_id}}
|
||||
{{/if}}
|
||||
|
||||
{{plugin-outlet name="edit-topic" args=(hash model=model buffered=buffered)}}
|
||||
<div class="edit-controls">
|
||||
{{d-button action=(action "finishedEditingTopic") class="btn-primary submit-edit" icon="check"}}
|
||||
{{d-button action=(action "cancelEditingTopic") class="btn-default cancel-edit" icon="times"}}
|
||||
|
||||
{{#if canRemoveTopicFeaturedLink}}
|
||||
<a href {{action "removeFeaturedLink"}} class="remove-featured-link" title="{{i18n "composer.remove_featured_link"}}">
|
||||
{{d-icon "times-circle"}}
|
||||
{{featuredLinkDomain}}
|
||||
</a>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{else}}
|
||||
<h1 data-topic-id="{{unbound model.id}}">
|
||||
{{#unless model.is_warning}}
|
||||
{{#if siteSettings.enable_personal_messages}}
|
||||
<a href={{pmPath}}>
|
||||
<span class="private-message-glyph">{{d-icon "envelope"}}</span>
|
||||
</a>
|
||||
{{else}}
|
||||
<span class="private-message-glyph">{{d-icon "envelope"}}</span>
|
||||
{{/if}}
|
||||
{{/unless}}
|
||||
|
||||
{{#if model.details.loaded}}
|
||||
{{topic-status topic=model}}
|
||||
<a href="{{unbound model.url}}" {{action "jumpTop"}} class="fancy-title">
|
||||
{{{model.fancyTitle}}}
|
||||
</a>
|
||||
{{/if}}
|
||||
|
||||
{{#if model.details.can_edit}}
|
||||
<a href {{action "editTopic"}} class="edit-topic" title="{{i18n "edit"}}">{{d-icon "pencil-alt"}}</a>
|
||||
{{/if}}
|
||||
</h1>
|
||||
|
||||
{{topic-category topic=model class="topic-category"}}
|
||||
{{/if}}
|
||||
{{/topic-title}}
|
||||
{{#if model.view_hidden}}
|
||||
{{topic-join-group-notice model=model action=(action "joinGroup")}}
|
||||
{{else}}
|
||||
{{#if model}}
|
||||
{{add-category-tag-classes category=model.category tags=model.tags}}
|
||||
<div class="container">
|
||||
{{discourse-banner user=currentUser banner=site.banner overlay=hasScrolled hide=model.errorLoading}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if showSharedDraftControls}}
|
||||
{{shared-draft-controls topic=model}}
|
||||
{{/if}}
|
||||
|
||||
<div class="container posts">
|
||||
<div class='selected-posts {{unless multiSelect 'hidden'}}'>
|
||||
{{partial "selected-posts"}}
|
||||
</div>
|
||||
{{plugin-outlet name="topic-above-post-stream" args=(hash model=model)}}
|
||||
|
||||
{{#topic-navigation topic=model jumpToDate=(action "jumpToDate") jumpToIndex=(action "jumpToIndex") as |info|}}
|
||||
{{#if info.renderTimeline}}
|
||||
{{#if info.renderAdminMenuButton}}
|
||||
{{topic-admin-menu-button
|
||||
topic=model
|
||||
fixed="true"
|
||||
toggleMultiSelect=(action "toggleMultiSelect")
|
||||
hideMultiSelect=(action "hideMultiSelect")
|
||||
deleteTopic=(action "deleteTopic")
|
||||
recoverTopic=(action "recoverTopic")
|
||||
toggleClosed=(action "toggleClosed")
|
||||
toggleArchived=(action "toggleArchived")
|
||||
toggleVisibility=(action "toggleVisibility")
|
||||
showTopicStatusUpdate=(route-action "showTopicStatusUpdate")
|
||||
showFeatureTopic=(route-action "showFeatureTopic")
|
||||
showChangeTimestamp=(route-action "showChangeTimestamp")
|
||||
resetBumpDate=(action "resetBumpDate")
|
||||
convertToPublicTopic=(action "convertToPublicTopic")
|
||||
convertToPrivateMessage=(action "convertToPrivateMessage")}}
|
||||
{{#if model.postStream.loaded}}
|
||||
{{#if model.postStream.firstPostPresent}}
|
||||
{{#topic-title cancelled=(action "cancelEditingTopic") save=(action "finishedEditingTopic") model=model}}
|
||||
{{#if editingTopic}}
|
||||
<div class="edit-topic-title">
|
||||
{{#if model.isPrivateMessage}}
|
||||
<span class="private-message-glyph">{{d-icon "envelope"}}</span>
|
||||
{{/if}}
|
||||
{{text-field id="edit-title" value=buffered.title maxlength=siteSettings.max_topic_title_length autofocus="true"}}
|
||||
{{#if showCategoryChooser}}
|
||||
{{category-chooser
|
||||
class="small"
|
||||
value=(unbound buffered.category_id)
|
||||
onSelectAny=(action "topicCategoryChanged")}}
|
||||
{{/if}}
|
||||
|
||||
{{#if canEditTags}}
|
||||
{{mini-tag-chooser filterable=true tags=buffered.tags categoryId=buffered.category_id}}
|
||||
{{/if}}
|
||||
|
||||
{{plugin-outlet name="edit-topic" args=(hash model=model buffered=buffered)}}
|
||||
<div class="edit-controls">
|
||||
{{d-button action=(action "finishedEditingTopic") class="btn-primary submit-edit" icon="check"}}
|
||||
{{d-button action=(action "cancelEditingTopic") class="btn-default cancel-edit" icon="times"}}
|
||||
|
||||
{{#if canRemoveTopicFeaturedLink}}
|
||||
<a href {{action "removeFeaturedLink"}} class="remove-featured-link" title="{{i18n "composer.remove_featured_link"}}">
|
||||
{{d-icon "times-circle"}}
|
||||
{{featuredLinkDomain}}
|
||||
</a>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{else}}
|
||||
<h1 data-topic-id="{{unbound model.id}}">
|
||||
{{#unless model.is_warning}}
|
||||
{{#if siteSettings.enable_personal_messages}}
|
||||
{{#if model.isPrivateMessage}}
|
||||
<a href={{pmPath}} title="{{i18n 'topic_statuses.personal_message.title'}}" aria-label={{i18n 'user.messages.inbox'}}>
|
||||
<span class="private-message-glyph">{{d-icon "envelope"}}</span>
|
||||
</a>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
{{#if model.isPrivateMessage}}
|
||||
<span class="private-message-glyph">{{d-icon "envelope"}}</span>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/unless}}
|
||||
|
||||
{{#if model.details.loaded}}
|
||||
{{topic-status topic=model}}
|
||||
<a href="{{unbound model.url}}" {{action "jumpTop"}} class="fancy-title">
|
||||
{{{model.fancyTitle}}}
|
||||
</a>
|
||||
{{/if}}
|
||||
|
||||
{{#if model.details.can_edit}}
|
||||
<a href {{action "editTopic"}} class="edit-topic" title="{{i18n "edit"}}">{{d-icon "pencil-alt"}}</a>
|
||||
{{/if}}
|
||||
</h1>
|
||||
|
||||
{{topic-category topic=model class="topic-category"}}
|
||||
{{/if}}
|
||||
{{/topic-title}}
|
||||
{{/if}}
|
||||
|
||||
{{topic-timeline
|
||||
topic=model
|
||||
notificationLevel=model.details.notification_level
|
||||
prevEvent=info.prevEvent
|
||||
fullscreen=info.topicProgressExpanded
|
||||
enteredIndex=enteredIndex
|
||||
loading=model.postStream.loading
|
||||
jumpToPost=(action "jumpToPost")
|
||||
jumpTop=(action "jumpTop")
|
||||
jumpBottom=(action "jumpBottom")
|
||||
jumpToPostPrompt=(action "jumpToPostPrompt")
|
||||
jumpToIndex=(action "jumpToIndex")
|
||||
replyToPost=(action "replyToPost")
|
||||
toggleMultiSelect=(action "toggleMultiSelect")
|
||||
hideMultiSelect=(action "hideMultiSelect")
|
||||
deleteTopic=(action "deleteTopic")
|
||||
recoverTopic=(action "recoverTopic")
|
||||
toggleClosed=(action "toggleClosed")
|
||||
toggleArchived=(action "toggleArchived")
|
||||
toggleVisibility=(action "toggleVisibility")
|
||||
showTopicStatusUpdate=(route-action "showTopicStatusUpdate")
|
||||
showFeatureTopic=(route-action "showFeatureTopic")
|
||||
showChangeTimestamp=(route-action "showChangeTimestamp")
|
||||
resetBumpDate=(action "resetBumpDate")
|
||||
convertToPublicTopic=(action "convertToPublicTopic")
|
||||
convertToPrivateMessage=(action "convertToPrivateMessage")}}
|
||||
{{else}}
|
||||
{{#topic-progress
|
||||
prevEvent=info.prevEvent
|
||||
topic=model
|
||||
expanded=info.topicProgressExpanded
|
||||
jumpToPost=(action "jumpToPost")}}
|
||||
|
||||
<div class="container posts">
|
||||
<div class='selected-posts {{unless multiSelect 'hidden'}}'>
|
||||
{{partial "selected-posts"}}
|
||||
</div>
|
||||
|
||||
{{#topic-navigation topic=model jumpToDate=(action "jumpToDate") jumpToIndex=(action "jumpToIndex") as |info|}}
|
||||
{{#if info.renderTimeline}}
|
||||
{{#if info.renderAdminMenuButton}}
|
||||
{{topic-admin-menu-button
|
||||
topic=model
|
||||
openUpwards="true"
|
||||
rightSide="true"
|
||||
fixed="true"
|
||||
toggleMultiSelect=(action "toggleMultiSelect")
|
||||
hideMultiSelect=(action "hideMultiSelect")
|
||||
deleteTopic=(action "deleteTopic")
|
||||
@ -153,210 +108,265 @@
|
||||
convertToPublicTopic=(action "convertToPublicTopic")
|
||||
convertToPrivateMessage=(action "convertToPrivateMessage")}}
|
||||
{{/if}}
|
||||
{{/topic-progress}}
|
||||
{{/if}}
|
||||
{{/topic-navigation}}
|
||||
|
||||
<div class="row">
|
||||
<section class="topic-area" id="topic" data-topic-id="{{unbound model.id}}">
|
||||
{{topic-timeline
|
||||
topic=model
|
||||
notificationLevel=model.details.notification_level
|
||||
prevEvent=info.prevEvent
|
||||
fullscreen=info.topicProgressExpanded
|
||||
enteredIndex=enteredIndex
|
||||
loading=model.postStream.loading
|
||||
jumpToPost=(action "jumpToPost")
|
||||
jumpTop=(action "jumpTop")
|
||||
jumpBottom=(action "jumpBottom")
|
||||
jumpToPostPrompt=(action "jumpToPostPrompt")
|
||||
jumpToIndex=(action "jumpToIndex")
|
||||
replyToPost=(action "replyToPost")
|
||||
toggleMultiSelect=(action "toggleMultiSelect")
|
||||
hideMultiSelect=(action "hideMultiSelect")
|
||||
deleteTopic=(action "deleteTopic")
|
||||
recoverTopic=(action "recoverTopic")
|
||||
toggleClosed=(action "toggleClosed")
|
||||
toggleArchived=(action "toggleArchived")
|
||||
toggleVisibility=(action "toggleVisibility")
|
||||
showTopicStatusUpdate=(route-action "showTopicStatusUpdate")
|
||||
showFeatureTopic=(route-action "showFeatureTopic")
|
||||
showChangeTimestamp=(route-action "showChangeTimestamp")
|
||||
resetBumpDate=(action "resetBumpDate")
|
||||
convertToPublicTopic=(action "convertToPublicTopic")
|
||||
convertToPrivateMessage=(action "convertToPrivateMessage")}}
|
||||
{{else}}
|
||||
{{#topic-progress
|
||||
prevEvent=info.prevEvent
|
||||
topic=model
|
||||
expanded=info.topicProgressExpanded
|
||||
jumpToPost=(action "jumpToPost")}}
|
||||
{{#if info.renderAdminMenuButton}}
|
||||
{{topic-admin-menu-button
|
||||
topic=model
|
||||
openUpwards="true"
|
||||
rightSide="true"
|
||||
toggleMultiSelect=(action "toggleMultiSelect")
|
||||
hideMultiSelect=(action "hideMultiSelect")
|
||||
deleteTopic=(action "deleteTopic")
|
||||
recoverTopic=(action "recoverTopic")
|
||||
toggleClosed=(action "toggleClosed")
|
||||
toggleArchived=(action "toggleArchived")
|
||||
toggleVisibility=(action "toggleVisibility")
|
||||
showTopicStatusUpdate=(route-action "showTopicStatusUpdate")
|
||||
showFeatureTopic=(route-action "showFeatureTopic")
|
||||
showChangeTimestamp=(route-action "showChangeTimestamp")
|
||||
resetBumpDate=(action "resetBumpDate")
|
||||
convertToPublicTopic=(action "convertToPublicTopic")
|
||||
convertToPrivateMessage=(action "convertToPrivateMessage")}}
|
||||
{{/if}}
|
||||
{{/topic-progress}}
|
||||
{{/if}}
|
||||
{{/topic-navigation}}
|
||||
|
||||
<div class="posts-wrapper">
|
||||
{{conditional-loading-spinner condition=model.postStream.loadingAbove}}
|
||||
<div class="row">
|
||||
<section class="topic-area" id="topic" data-topic-id="{{unbound model.id}}">
|
||||
|
||||
{{plugin-outlet name="topic-above-posts" args=(hash model=model)}}
|
||||
<div class="posts-wrapper">
|
||||
{{conditional-loading-spinner condition=model.postStream.loadingAbove}}
|
||||
|
||||
{{#unless model.postStream.loadingFilter}}
|
||||
{{scrolling-post-stream
|
||||
posts=postsToRender
|
||||
canCreatePost=model.details.can_create_post
|
||||
multiSelect=multiSelect
|
||||
selectedPostsCount=selectedPostsCount
|
||||
selectedQuery=selectedQuery
|
||||
gaps=model.postStream.gaps
|
||||
showFlags=(action "showPostFlags")
|
||||
editPost=(action "editPost")
|
||||
showHistory=(route-action "showHistory")
|
||||
showLogin=(route-action "showLogin")
|
||||
showRawEmail=(route-action "showRawEmail")
|
||||
deletePost=(action "deletePost")
|
||||
recoverPost=(action "recoverPost")
|
||||
expandHidden=(action "expandHidden")
|
||||
newTopicAction=(action "replyAsNewTopic")
|
||||
toggleBookmark=(action "toggleBookmark")
|
||||
togglePostType=(action "togglePostType")
|
||||
rebakePost=(action "rebakePost")
|
||||
changePostOwner=(action "changePostOwner")
|
||||
grantBadge=(action "grantBadge")
|
||||
addNotice=(action "addNotice")
|
||||
removeNotice=(action "removeNotice")
|
||||
lockPost=(action "lockPost")
|
||||
unlockPost=(action "unlockPost")
|
||||
unhidePost=(action "unhidePost")
|
||||
replyToPost=(action "replyToPost")
|
||||
toggleWiki=(action "toggleWiki")
|
||||
toggleSummary=(action "toggleSummary")
|
||||
removeAllowedUser=(action "removeAllowedUser")
|
||||
removeAllowedGroup=(action "removeAllowedGroup")
|
||||
topVisibleChanged=(action "topVisibleChanged")
|
||||
currentPostChanged=(action "currentPostChanged")
|
||||
currentPostScrolled=(action "currentPostScrolled")
|
||||
bottomVisibleChanged=(action "bottomVisibleChanged")
|
||||
togglePostSelection=(action "togglePostSelection")
|
||||
selectReplies=(action "selectReplies")
|
||||
selectBelow=(action "selectBelow")
|
||||
fillGapBefore=(action "fillGapBefore")
|
||||
fillGapAfter=(action "fillGapAfter")
|
||||
showInvite=(route-action "showInvite")}}
|
||||
{{/unless}}
|
||||
{{plugin-outlet name="topic-above-posts" args=(hash model=model)}}
|
||||
|
||||
{{conditional-loading-spinner condition=model.postStream.loadingBelow}}
|
||||
</div>
|
||||
<div id="topic-bottom"></div>
|
||||
{{#unless model.postStream.loadingFilter}}
|
||||
{{scrolling-post-stream
|
||||
posts=postsToRender
|
||||
canCreatePost=model.details.can_create_post
|
||||
multiSelect=multiSelect
|
||||
selectedPostsCount=selectedPostsCount
|
||||
selectedQuery=selectedQuery
|
||||
gaps=model.postStream.gaps
|
||||
showFlags=(action "showPostFlags")
|
||||
editPost=(action "editPost")
|
||||
showHistory=(route-action "showHistory")
|
||||
showLogin=(route-action "showLogin")
|
||||
showRawEmail=(route-action "showRawEmail")
|
||||
deletePost=(action "deletePost")
|
||||
recoverPost=(action "recoverPost")
|
||||
expandHidden=(action "expandHidden")
|
||||
newTopicAction=(action "replyAsNewTopic")
|
||||
toggleBookmark=(action "toggleBookmark")
|
||||
togglePostType=(action "togglePostType")
|
||||
rebakePost=(action "rebakePost")
|
||||
changePostOwner=(action "changePostOwner")
|
||||
grantBadge=(action "grantBadge")
|
||||
addNotice=(action "addNotice")
|
||||
removeNotice=(action "removeNotice")
|
||||
lockPost=(action "lockPost")
|
||||
unlockPost=(action "unlockPost")
|
||||
unhidePost=(action "unhidePost")
|
||||
replyToPost=(action "replyToPost")
|
||||
toggleWiki=(action "toggleWiki")
|
||||
toggleSummary=(action "toggleSummary")
|
||||
removeAllowedUser=(action "removeAllowedUser")
|
||||
removeAllowedGroup=(action "removeAllowedGroup")
|
||||
topVisibleChanged=(action "topVisibleChanged")
|
||||
currentPostChanged=(action "currentPostChanged")
|
||||
currentPostScrolled=(action "currentPostScrolled")
|
||||
bottomVisibleChanged=(action "bottomVisibleChanged")
|
||||
togglePostSelection=(action "togglePostSelection")
|
||||
selectReplies=(action "selectReplies")
|
||||
selectBelow=(action "selectBelow")
|
||||
fillGapBefore=(action "fillGapBefore")
|
||||
fillGapAfter=(action "fillGapAfter")
|
||||
showInvite=(route-action "showInvite")}}
|
||||
{{/unless}}
|
||||
|
||||
{{#conditional-loading-spinner condition=model.postStream.loadingFilter}}
|
||||
{{#if loadedAllPosts}}
|
||||
{{conditional-loading-spinner condition=model.postStream.loadingBelow}}
|
||||
</div>
|
||||
<div id="topic-bottom"></div>
|
||||
|
||||
{{#if model.pending_posts}}
|
||||
<div class='pending-posts'>
|
||||
{{#each model.pending_posts as |pending|}}
|
||||
<div class='reviewable-item'>
|
||||
<div class='reviewable-meta-data'>
|
||||
<span class='reviewable-type'>
|
||||
{{i18n "review.awaiting_approval"}}
|
||||
</span>
|
||||
<span class='created-at'>
|
||||
{{age-with-tooltip pending.created_at}}
|
||||
</span>
|
||||
</div>
|
||||
<div class='post-contents-wrapper'>
|
||||
{{reviewable-created-by user=currentUser tagName=''}}
|
||||
<div class='post-contents'>
|
||||
{{reviewable-created-by-name user=currentUser tagName=''}}
|
||||
<div class='post-body'>{{cook-text pending.raw}}</div>
|
||||
{{#conditional-loading-spinner condition=model.postStream.loadingFilter}}
|
||||
{{#if loadedAllPosts}}
|
||||
|
||||
{{#if model.pending_posts}}
|
||||
<div class='pending-posts'>
|
||||
{{#each model.pending_posts as |pending|}}
|
||||
<div class='reviewable-item'>
|
||||
<div class='reviewable-meta-data'>
|
||||
<span class='reviewable-type'>
|
||||
{{i18n "review.awaiting_approval"}}
|
||||
</span>
|
||||
<span class='created-at'>
|
||||
{{age-with-tooltip pending.created_at}}
|
||||
</span>
|
||||
</div>
|
||||
<div class='post-contents-wrapper'>
|
||||
{{reviewable-created-by user=currentUser tagName=''}}
|
||||
<div class='post-contents'>
|
||||
{{reviewable-created-by-name user=currentUser tagName=''}}
|
||||
<div class='post-body'>{{cook-text pending.raw}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class='reviewable-actions'>
|
||||
{{d-button
|
||||
class="btn-danger"
|
||||
label="review.delete"
|
||||
icon="trash-alt"
|
||||
action=(action "deletePending" pending) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class='reviewable-actions'>
|
||||
{{d-button
|
||||
class="btn-danger"
|
||||
label="review.delete"
|
||||
icon="trash-alt"
|
||||
action=(action "deletePending" pending) }}
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if model.queued_posts_count}}
|
||||
<div class="has-pending-posts">
|
||||
<div>
|
||||
{{{i18n "review.topic_has_pending" count=model.queued_posts_count}}}
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#link-to 'review' (query-params topic_id=model.id type="ReviewableQueuedPost" status="pending")}}
|
||||
{{i18n "review.view_pending"}}
|
||||
{{/link-to}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if model.queued_posts_count}}
|
||||
<div class="has-pending-posts">
|
||||
<div>
|
||||
{{{i18n "review.topic_has_pending" count=model.queued_posts_count}}}
|
||||
</div>
|
||||
|
||||
{{#link-to 'review' (query-params topic_id=model.id type="ReviewableQueuedPost" status="pending")}}
|
||||
{{i18n "review.view_pending"}}
|
||||
{{/link-to}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if model.private_topic_timer.execute_at}}
|
||||
{{topic-timer-info
|
||||
topicClosed=model.closed
|
||||
statusType=model.private_topic_timer.status_type
|
||||
executeAt=model.private_topic_timer.execute_at
|
||||
duration=model.private_topic_timer.duration
|
||||
removeTopicTimer=(action "removeTopicTimer" model.private_topic_timer.status_type "private_topic_timer")}}
|
||||
{{/if}}
|
||||
|
||||
{{#if model.private_topic_timer.execute_at}}
|
||||
{{topic-timer-info
|
||||
topicClosed=model.closed
|
||||
statusType=model.private_topic_timer.status_type
|
||||
executeAt=model.private_topic_timer.execute_at
|
||||
duration=model.private_topic_timer.duration}}
|
||||
{{/if}}
|
||||
statusType=model.topic_timer.status_type
|
||||
executeAt=model.topic_timer.execute_at
|
||||
basedOnLastPost=model.topic_timer.based_on_last_post
|
||||
duration=model.topic_timer.duration
|
||||
categoryId=model.topic_timer.category_id
|
||||
removeTopicTimer=(action "removeTopicTimer" model.topic_timer.status_type "topic_timer")}}
|
||||
|
||||
{{topic-timer-info
|
||||
topicClosed=model.closed
|
||||
statusType=model.topic_timer.status_type
|
||||
executeAt=model.topic_timer.execute_at
|
||||
basedOnLastPost=model.topic_timer.based_on_last_post
|
||||
duration=model.topic_timer.duration
|
||||
categoryId=model.topic_timer.category_id}}
|
||||
|
||||
{{#if session.showSignupCta}}
|
||||
{{! replace "Log In to Reply" with the infobox }}
|
||||
{{signup-cta}}
|
||||
{{else}}
|
||||
{{#if currentUser}}
|
||||
{{plugin-outlet name="topic-above-footer-buttons" args=(hash model=model)}}
|
||||
|
||||
{{topic-footer-buttons
|
||||
topic=model
|
||||
toggleMultiSelect=(action "toggleMultiSelect")
|
||||
hideMultiSelect=(action "hideMultiSelect")
|
||||
deleteTopic=(action "deleteTopic")
|
||||
recoverTopic=(action "recoverTopic")
|
||||
toggleClosed=(action "toggleClosed")
|
||||
toggleArchived=(action "toggleArchived")
|
||||
toggleVisibility=(action "toggleVisibility")
|
||||
showTopicStatusUpdate=(route-action "showTopicStatusUpdate")
|
||||
showFeatureTopic=(route-action "showFeatureTopic")
|
||||
showChangeTimestamp=(route-action "showChangeTimestamp")
|
||||
resetBumpDate=(action "resetBumpDate")
|
||||
convertToPublicTopic=(action "convertToPublicTopic")
|
||||
convertToPrivateMessage=(action "convertToPrivateMessage")
|
||||
toggleBookmark=(action "toggleBookmark")
|
||||
showFlagTopic=(route-action "showFlagTopic")
|
||||
toggleArchiveMessage=(action "toggleArchiveMessage")
|
||||
editFirstPost=(action "editFirstPost")
|
||||
deferTopic=(action "deferTopic")
|
||||
replyToPost=(action "replyToPost")}}
|
||||
{{#if session.showSignupCta}}
|
||||
{{! replace "Log In to Reply" with the infobox }}
|
||||
{{signup-cta}}
|
||||
{{else}}
|
||||
<div id="topic-footer-buttons">
|
||||
{{d-button icon="reply" class="btn-primary pull-right" action=(route-action "showLogin") label="topic.reply.title"}}
|
||||
{{#if currentUser}}
|
||||
{{plugin-outlet name="topic-above-footer-buttons" args=(hash model=model)}}
|
||||
|
||||
{{topic-footer-buttons
|
||||
topic=model
|
||||
toggleMultiSelect=(action "toggleMultiSelect")
|
||||
hideMultiSelect=(action "hideMultiSelect")
|
||||
deleteTopic=(action "deleteTopic")
|
||||
recoverTopic=(action "recoverTopic")
|
||||
toggleClosed=(action "toggleClosed")
|
||||
toggleArchived=(action "toggleArchived")
|
||||
toggleVisibility=(action "toggleVisibility")
|
||||
showTopicStatusUpdate=(route-action "showTopicStatusUpdate")
|
||||
showFeatureTopic=(route-action "showFeatureTopic")
|
||||
showChangeTimestamp=(route-action "showChangeTimestamp")
|
||||
resetBumpDate=(action "resetBumpDate")
|
||||
convertToPublicTopic=(action "convertToPublicTopic")
|
||||
convertToPrivateMessage=(action "convertToPrivateMessage")
|
||||
toggleBookmark=(action "toggleBookmark")
|
||||
showFlagTopic=(route-action "showFlagTopic")
|
||||
toggleArchiveMessage=(action "toggleArchiveMessage")
|
||||
editFirstPost=(action "editFirstPost")
|
||||
deferTopic=(action "deferTopic")
|
||||
replyToPost=(action "replyToPost")}}
|
||||
{{else}}
|
||||
<div id="topic-footer-buttons">
|
||||
{{d-button icon="reply" class="btn-primary pull-right" action=(route-action "showLogin") label="topic.reply.title"}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#if showSelectedPostsAtBottom}}
|
||||
<div class='selected-posts {{unless multiSelect 'hidden'}}'>
|
||||
{{partial "selected-posts"}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#if showSelectedPostsAtBottom}}
|
||||
<div class='selected-posts {{unless multiSelect 'hidden'}}'>
|
||||
{{partial "selected-posts"}}
|
||||
{{plugin-outlet name="topic-above-suggested" args=(hash model=model)}}
|
||||
<div class="{{if model.relatedMessages.length 'related-messages-wrapper'}} {{if model.suggestedTopics.length 'suggested-topics-wrapper'}}">
|
||||
{{#if model.relatedMessages.length}}
|
||||
{{related-messages topic=model}}
|
||||
{{/if}}
|
||||
{{#if model.suggestedTopics.length}}
|
||||
{{suggested-topics topic=model}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/conditional-loading-spinner}}
|
||||
|
||||
{{plugin-outlet name="topic-above-suggested" args=(hash model=model)}}
|
||||
<div class="{{if model.relatedMessages.length 'related-messages-wrapper'}} {{if model.suggestedTopics.length 'suggested-topics-wrapper'}}">
|
||||
{{#if model.relatedMessages.length}}
|
||||
{{related-messages topic=model}}
|
||||
{{/if}}
|
||||
{{#if model.suggestedTopics.length}}
|
||||
{{suggested-topics topic=model}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/conditional-loading-spinner}}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="container">
|
||||
{{#conditional-loading-spinner condition=noErrorYet}}
|
||||
{{#if model.notFoundHtml}}
|
||||
<div class="not-found">{{{model.notFoundHtml}}}</div>
|
||||
{{else}}
|
||||
<div class="topic-error">
|
||||
<div>{{model.message}}</div>
|
||||
{{#if model.noRetry}}
|
||||
{{#unless currentUser}}
|
||||
{{d-button action=(route-action "showLogin") class="btn-primary topic-retry" icon="user" label="log_in"}}
|
||||
{{/unless}}
|
||||
{{else}}
|
||||
{{d-button action=(action "retryLoading") class="btn-primary topic-retry" icon="sync" label="errors.buttons.again"}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{conditional-loading-spinner condition=retrying}}
|
||||
{{/if}}
|
||||
{{/conditional-loading-spinner}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="container">
|
||||
{{#conditional-loading-spinner condition=noErrorYet}}
|
||||
{{#if model.notFoundHtml}}
|
||||
<div class="not-found">{{{model.notFoundHtml}}}</div>
|
||||
{{else}}
|
||||
<div class="topic-error">
|
||||
<div>{{model.message}}</div>
|
||||
{{#if model.noRetry}}
|
||||
{{#unless currentUser}}
|
||||
{{d-button action=(route-action "showLogin") class="btn-primary topic-retry" icon="user" label="log_in"}}
|
||||
{{/unless}}
|
||||
{{else}}
|
||||
{{d-button action=(action "retryLoading") class="btn-primary topic-retry" icon="sync" label="errors.buttons.again"}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{conditional-loading-spinner condition=retrying}}
|
||||
{{/if}}
|
||||
{{/conditional-loading-spinner}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{share-popup topic=model replyAsNewTopic=(action "replyAsNewTopic")}}
|
||||
|
||||
{{share-popup topic=model replyAsNewTopic=(action "replyAsNewTopic")}}
|
||||
|
||||
{{#if embedQuoteButton}}
|
||||
{{quote-button quoteState=quoteState selectText=(action "selectText")}}
|
||||
{{#if embedQuoteButton}}
|
||||
{{quote-button quoteState=quoteState selectText=(action "selectText")}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/discourse-topic}}
|
||||
|
||||
@ -16,9 +16,7 @@ export default class Connector {
|
||||
|
||||
if (opts.templateName) {
|
||||
deprecated(
|
||||
`Using a 'templateName' for a connector is deprecated. Use 'component' instead [${
|
||||
opts.templateName
|
||||
}]`
|
||||
`Using a 'templateName' for a connector is deprecated. Use 'component' instead [${opts.templateName}]`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,21 @@
|
||||
import { createWidgetFrom } from "discourse/widgets/widget";
|
||||
import { DefaultNotificationItem } from "discourse/widgets/default-notification-item";
|
||||
import { formatUsername } from "discourse/lib/utilities";
|
||||
import { iconNode } from "discourse-common/lib/icon-library";
|
||||
|
||||
createWidgetFrom(DefaultNotificationItem, "custom-notification-item", {
|
||||
notificationTitle(notificationName, data) {
|
||||
return data.title ? I18n.t(data.title) : "";
|
||||
},
|
||||
|
||||
text(notificationName, data) {
|
||||
const username = formatUsername(data.display_username);
|
||||
const description = this.description(data);
|
||||
|
||||
return I18n.t(data.message, { description, username });
|
||||
},
|
||||
|
||||
icon(notificationName, data) {
|
||||
return iconNode(`notification.${data.message}`);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,158 @@
|
||||
import { wantsNewWindow } from "discourse/lib/intercept-click";
|
||||
import RawHtml from "discourse/widgets/raw-html";
|
||||
import { createWidget } from "discourse/widgets/widget";
|
||||
import DiscourseURL from "discourse/lib/url";
|
||||
import { h } from "virtual-dom";
|
||||
import { emojiUnescape } from "discourse/lib/text";
|
||||
import {
|
||||
postUrl,
|
||||
escapeExpression,
|
||||
formatUsername
|
||||
} from "discourse/lib/utilities";
|
||||
import { setTransientHeader } from "discourse/lib/ajax";
|
||||
import { userPath } from "discourse/lib/url";
|
||||
import { iconNode } from "discourse-common/lib/icon-library";
|
||||
|
||||
export const DefaultNotificationItem = createWidget(
|
||||
"default-notification-item",
|
||||
{
|
||||
tagName: "li",
|
||||
|
||||
buildClasses(attrs) {
|
||||
const classNames = [];
|
||||
if (attrs.get("read")) {
|
||||
classNames.push("read");
|
||||
}
|
||||
if (attrs.is_warning) {
|
||||
classNames.push("is-warning");
|
||||
}
|
||||
return classNames;
|
||||
},
|
||||
|
||||
url(data) {
|
||||
const attrs = this.attrs;
|
||||
|
||||
const badgeId = data.badge_id;
|
||||
if (badgeId) {
|
||||
let badgeSlug = data.badge_slug;
|
||||
|
||||
if (!badgeSlug) {
|
||||
const badgeName = data.badge_name;
|
||||
badgeSlug = badgeName.replace(/[^A-Za-z0-9_]+/g, "-").toLowerCase();
|
||||
}
|
||||
|
||||
let username = data.username;
|
||||
username = username ? "?username=" + username.toLowerCase() : "";
|
||||
return Discourse.getURL(
|
||||
"/badges/" + badgeId + "/" + badgeSlug + username
|
||||
);
|
||||
}
|
||||
|
||||
const topicId = attrs.topic_id;
|
||||
|
||||
if (topicId) {
|
||||
return postUrl(attrs.slug, topicId, attrs.post_number);
|
||||
}
|
||||
|
||||
if (data.group_id) {
|
||||
return userPath(data.username + "/messages/group/" + data.group_name);
|
||||
}
|
||||
},
|
||||
|
||||
description(data) {
|
||||
const badgeName = data.badge_name;
|
||||
if (badgeName) {
|
||||
return escapeExpression(badgeName);
|
||||
}
|
||||
|
||||
if (this.attrs.fancy_title) {
|
||||
if (this.attrs.topic_id) {
|
||||
return `<span data-topic-id="${this.attrs.topic_id}">${this.attrs.fancy_title}</span>`;
|
||||
}
|
||||
return this.attrs.fancy_title;
|
||||
}
|
||||
|
||||
const description = data.topic_title;
|
||||
|
||||
return Ember.isEmpty(description) ? "" : escapeExpression(description);
|
||||
},
|
||||
|
||||
text(notificationName, data) {
|
||||
const username = formatUsername(data.display_username);
|
||||
const description = this.description(data);
|
||||
|
||||
return I18n.t(`notifications.${notificationName}`, {
|
||||
description,
|
||||
username
|
||||
});
|
||||
},
|
||||
|
||||
icon(notificationName) {
|
||||
return iconNode(`notification.${notificationName}`);
|
||||
},
|
||||
|
||||
notificationTitle(notificationName) {
|
||||
if (notificationName) {
|
||||
return I18n.t(`notifications.titles.${notificationName}`);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
},
|
||||
|
||||
html(attrs) {
|
||||
const notificationType = attrs.notification_type;
|
||||
const lookup = this.site.get("notificationLookup");
|
||||
const notificationName = lookup[notificationType];
|
||||
|
||||
let { data } = attrs;
|
||||
let text = emojiUnescape(this.text(notificationName, data));
|
||||
let icon = this.icon(notificationName, data);
|
||||
|
||||
const title = this.notificationTitle(notificationName, data);
|
||||
|
||||
// We can use a `<p>` tag here once other languages have fixed their HTML
|
||||
// translations.
|
||||
let html = new RawHtml({ html: `<div>${text}</div>` });
|
||||
|
||||
let contents = [icon, html];
|
||||
|
||||
const href = this.url(data);
|
||||
return href
|
||||
? h(
|
||||
"a",
|
||||
{ attributes: { href, title, "data-auto-route": true } },
|
||||
contents
|
||||
)
|
||||
: contents;
|
||||
},
|
||||
|
||||
click(e) {
|
||||
this.attrs.set("read", true);
|
||||
const id = this.attrs.id;
|
||||
setTransientHeader("Discourse-Clear-Notifications", id);
|
||||
if (document && document.cookie) {
|
||||
let path = Discourse.BaseUri || "/";
|
||||
document.cookie = `cn=${id}; path=${path}; expires=Fri, 31 Dec 9999 23:59:59 GMT`;
|
||||
}
|
||||
if (wantsNewWindow(e)) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
|
||||
this.sendWidgetEvent("linkClicked");
|
||||
DiscourseURL.routeTo(this.url(this.attrs.data), {
|
||||
afterRouteComplete: () => {
|
||||
if (!this.attrs.data.revision_number) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.appEvents.trigger(
|
||||
"post:show-revision",
|
||||
this.attrs.post_number,
|
||||
this.attrs.data.revision_number
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
@ -0,0 +1,10 @@
|
||||
import RawHtml from "discourse/widgets/raw-html";
|
||||
import renderTags from "discourse/lib/render-tags";
|
||||
|
||||
// Right now it's RawHTML. Eventually it should emit nodes
|
||||
export default class DiscourseTags extends RawHtml {
|
||||
constructor(attrs) {
|
||||
attrs.html = renderTags(attrs.topic, attrs);
|
||||
super(attrs);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
import { createWidgetFrom } from "discourse/widgets/widget";
|
||||
import { DefaultNotificationItem } from "discourse/widgets/default-notification-item";
|
||||
|
||||
createWidgetFrom(
|
||||
DefaultNotificationItem,
|
||||
"group-message-summary-notification-item",
|
||||
{
|
||||
text(notificationName, data) {
|
||||
const count = data.inbox_count;
|
||||
const group_name = data.group_name;
|
||||
|
||||
return I18n.t("notifications.group_message_summary", {
|
||||
count,
|
||||
group_name
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
@ -59,7 +59,7 @@ export default createWidget("hamburger-menu", {
|
||||
|
||||
if (currentUser.admin) {
|
||||
links.push({
|
||||
href: "/admin/site_settings/category/required",
|
||||
href: "/admin/site_settings",
|
||||
icon: "cog",
|
||||
label: "admin.site_settings.title",
|
||||
className: "settings-link"
|
||||
|
||||
@ -399,9 +399,7 @@ export default createWidget("header", {
|
||||
var params = "";
|
||||
|
||||
if (context) {
|
||||
params = `?context=${context.type}&context_id=${
|
||||
context.id
|
||||
}&skip_context=${this.state.skipSearchContext}`;
|
||||
params = `?context=${context.type}&context_id=${context.id}&skip_context=${this.state.skipSearchContext}`;
|
||||
}
|
||||
|
||||
const currentPath = this.register
|
||||
|
||||
@ -0,0 +1,13 @@
|
||||
import { createWidgetFrom } from "discourse/widgets/widget";
|
||||
import { DefaultNotificationItem } from "discourse/widgets/default-notification-item";
|
||||
import { userPath } from "discourse/lib/url";
|
||||
|
||||
createWidgetFrom(
|
||||
DefaultNotificationItem,
|
||||
"invitee-accepted-notification-item",
|
||||
{
|
||||
url(data) {
|
||||
return userPath(data.display_username);
|
||||
}
|
||||
}
|
||||
);
|
||||
@ -0,0 +1,31 @@
|
||||
import { createWidgetFrom } from "discourse/widgets/widget";
|
||||
import { DefaultNotificationItem } from "discourse/widgets/default-notification-item";
|
||||
import { escapeExpression } from "discourse/lib/utilities";
|
||||
import { userPath } from "discourse/lib/url";
|
||||
|
||||
createWidgetFrom(
|
||||
DefaultNotificationItem,
|
||||
"liked-consolidated-notification-item",
|
||||
{
|
||||
url(data) {
|
||||
return userPath(
|
||||
`${this.attrs.username ||
|
||||
this.currentUser
|
||||
.username}/notifications/likes-received?acting_username=${
|
||||
data.display_username
|
||||
}`
|
||||
);
|
||||
},
|
||||
|
||||
description(data) {
|
||||
const description = I18n.t(
|
||||
"notifications.liked_consolidated_description",
|
||||
{
|
||||
count: parseInt(data.count)
|
||||
}
|
||||
);
|
||||
|
||||
return Ember.isEmpty(description) ? "" : escapeExpression(description);
|
||||
}
|
||||
}
|
||||
);
|
||||
@ -0,0 +1,32 @@
|
||||
import { createWidgetFrom } from "discourse/widgets/widget";
|
||||
import { DefaultNotificationItem } from "discourse/widgets/default-notification-item";
|
||||
import { formatUsername } from "discourse/lib/utilities";
|
||||
|
||||
createWidgetFrom(DefaultNotificationItem, "liked-notification-item", {
|
||||
text(notificationName, data) {
|
||||
const username = formatUsername(data.display_username);
|
||||
const description = this.description(data);
|
||||
|
||||
if (data.count > 1) {
|
||||
const count = data.count - 2;
|
||||
const username2 = formatUsername(data.username2);
|
||||
|
||||
if (count === 0) {
|
||||
return I18n.t("notifications.liked_2", {
|
||||
description,
|
||||
username,
|
||||
username2
|
||||
});
|
||||
} else {
|
||||
return I18n.t("notifications.liked_many", {
|
||||
description,
|
||||
username,
|
||||
username2,
|
||||
count
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return I18n.t("notifications.liked", { description, username });
|
||||
}
|
||||
});
|
||||
@ -1,215 +0,0 @@
|
||||
import { wantsNewWindow } from "discourse/lib/intercept-click";
|
||||
import RawHtml from "discourse/widgets/raw-html";
|
||||
import { createWidget } from "discourse/widgets/widget";
|
||||
import DiscourseURL from "discourse/lib/url";
|
||||
import { h } from "virtual-dom";
|
||||
import { emojiUnescape } from "discourse/lib/text";
|
||||
import {
|
||||
postUrl,
|
||||
escapeExpression,
|
||||
formatUsername
|
||||
} from "discourse/lib/utilities";
|
||||
import { setTransientHeader } from "discourse/lib/ajax";
|
||||
import { userPath } from "discourse/lib/url";
|
||||
import { iconNode } from "discourse-common/lib/icon-library";
|
||||
|
||||
createWidget("notification-item", {
|
||||
tagName: "li",
|
||||
|
||||
buildClasses(attrs) {
|
||||
const classNames = [];
|
||||
if (attrs.get("read")) {
|
||||
classNames.push("read");
|
||||
}
|
||||
if (attrs.is_warning) {
|
||||
classNames.push("is-warning");
|
||||
}
|
||||
return classNames;
|
||||
},
|
||||
|
||||
url() {
|
||||
const attrs = this.attrs;
|
||||
const data = attrs.data;
|
||||
const notificationTypes = this.site.notification_types;
|
||||
|
||||
const badgeId = data.badge_id;
|
||||
if (badgeId) {
|
||||
let badgeSlug = data.badge_slug;
|
||||
|
||||
if (!badgeSlug) {
|
||||
const badgeName = data.badge_name;
|
||||
badgeSlug = badgeName.replace(/[^A-Za-z0-9_]+/g, "-").toLowerCase();
|
||||
}
|
||||
|
||||
let username = data.username;
|
||||
username = username ? "?username=" + username.toLowerCase() : "";
|
||||
return Discourse.getURL(
|
||||
"/badges/" + badgeId + "/" + badgeSlug + username
|
||||
);
|
||||
}
|
||||
|
||||
const topicId = attrs.topic_id;
|
||||
|
||||
if (topicId) {
|
||||
return postUrl(attrs.slug, topicId, attrs.post_number);
|
||||
}
|
||||
|
||||
if (attrs.notification_type === notificationTypes.invitee_accepted) {
|
||||
return userPath(data.display_username);
|
||||
}
|
||||
|
||||
if (attrs.notification_type === notificationTypes.liked_consolidated) {
|
||||
return userPath(
|
||||
`${this.attrs.username ||
|
||||
this.currentUser
|
||||
.username}/notifications/likes-received?acting_username=${
|
||||
data.display_username
|
||||
}`
|
||||
);
|
||||
}
|
||||
|
||||
if (data.group_id) {
|
||||
return userPath(data.username + "/messages/group/" + data.group_name);
|
||||
}
|
||||
},
|
||||
|
||||
description() {
|
||||
const data = this.attrs.data;
|
||||
const badgeName = data.badge_name;
|
||||
if (badgeName) {
|
||||
return escapeExpression(badgeName);
|
||||
}
|
||||
|
||||
if (this.attrs.fancy_title) {
|
||||
if (this.attrs.topic_id) {
|
||||
return `<span data-topic-id="${this.attrs.topic_id}">${
|
||||
this.attrs.fancy_title
|
||||
}</span>`;
|
||||
}
|
||||
return this.attrs.fancy_title;
|
||||
}
|
||||
|
||||
let title;
|
||||
|
||||
if (
|
||||
this.attrs.notification_type ===
|
||||
this.site.notification_types.liked_consolidated
|
||||
) {
|
||||
title = I18n.t("notifications.liked_consolidated_description", {
|
||||
count: parseInt(data.count)
|
||||
});
|
||||
} else {
|
||||
title = data.topic_title;
|
||||
}
|
||||
|
||||
return Ember.isEmpty(title) ? "" : escapeExpression(title);
|
||||
},
|
||||
|
||||
text(notificationType, notName) {
|
||||
const { attrs } = this;
|
||||
const data = attrs.data;
|
||||
const scope =
|
||||
notName === "custom" ? data.message : `notifications.${notName}`;
|
||||
|
||||
const notificationTypes = this.site.notification_types;
|
||||
|
||||
if (notificationType === notificationTypes.group_message_summary) {
|
||||
const count = data.inbox_count;
|
||||
const group_name = data.group_name;
|
||||
return I18n.t(scope, { count, group_name });
|
||||
}
|
||||
|
||||
const username = formatUsername(data.display_username);
|
||||
const description = this.description();
|
||||
|
||||
if (notificationType === notificationTypes.liked && data.count > 1) {
|
||||
const count = data.count - 2;
|
||||
const username2 = formatUsername(data.username2);
|
||||
|
||||
if (count === 0) {
|
||||
return I18n.t("notifications.liked_2", {
|
||||
description,
|
||||
username,
|
||||
username2
|
||||
});
|
||||
} else {
|
||||
return I18n.t("notifications.liked_many", {
|
||||
description,
|
||||
username,
|
||||
username2,
|
||||
count
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return I18n.t(scope, { description, username });
|
||||
},
|
||||
|
||||
html(attrs) {
|
||||
const notificationType = attrs.notification_type;
|
||||
const lookup = this.site.get("notificationLookup");
|
||||
const notificationName = lookup[notificationType];
|
||||
|
||||
let { data } = attrs;
|
||||
let infoKey =
|
||||
notificationName === "custom" ? data.message : notificationName;
|
||||
let text = emojiUnescape(this.text(notificationType, notificationName));
|
||||
let icon = iconNode(`notification.${infoKey}`);
|
||||
|
||||
let title;
|
||||
|
||||
if (notificationName) {
|
||||
if (notificationName === "custom") {
|
||||
title = data.title ? I18n.t(data.title) : "";
|
||||
} else {
|
||||
title = I18n.t(`notifications.titles.${notificationName}`);
|
||||
}
|
||||
} else {
|
||||
title = "";
|
||||
}
|
||||
|
||||
// We can use a `<p>` tag here once other languages have fixed their HTML
|
||||
// translations.
|
||||
let html = new RawHtml({ html: `<div>${text}</div>` });
|
||||
|
||||
let contents = [icon, html];
|
||||
|
||||
const href = this.url();
|
||||
return href
|
||||
? h(
|
||||
"a",
|
||||
{ attributes: { href, title, "data-auto-route": true } },
|
||||
contents
|
||||
)
|
||||
: contents;
|
||||
},
|
||||
|
||||
click(e) {
|
||||
this.attrs.set("read", true);
|
||||
const id = this.attrs.id;
|
||||
setTransientHeader("Discourse-Clear-Notifications", id);
|
||||
if (document && document.cookie) {
|
||||
let path = Discourse.BaseUri || "/";
|
||||
document.cookie = `cn=${id}; path=${path}; expires=Fri, 31 Dec 9999 23:59:59 GMT`;
|
||||
}
|
||||
if (wantsNewWindow(e)) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
|
||||
this.sendWidgetEvent("linkClicked");
|
||||
DiscourseURL.routeTo(this.url(), {
|
||||
afterRouteComplete: () => {
|
||||
if (!this.attrs.data.revision_number) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.appEvents.trigger(
|
||||
"post:show-revision",
|
||||
this.attrs.post_number,
|
||||
this.attrs.data.revision_number
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -61,9 +61,15 @@ function likeCount(attrs) {
|
||||
? "post.has_likes_title_only_you"
|
||||
: "post.has_likes_title_you"
|
||||
: "post.has_likes_title";
|
||||
const icon = attrs.yours ? "d-liked" : "";
|
||||
let icon = attrs.yours ? "d-liked" : "";
|
||||
let addContainer = attrs.yours;
|
||||
const additionalClass = attrs.yours ? "my-likes" : "regular-likes";
|
||||
|
||||
if (!attrs.showLike) {
|
||||
icon = attrs.yours ? "d-liked" : "d-unliked";
|
||||
addContainer = true;
|
||||
}
|
||||
|
||||
return {
|
||||
action: "toggleWhoLiked",
|
||||
title,
|
||||
@ -71,7 +77,7 @@ function likeCount(attrs) {
|
||||
contents: count,
|
||||
icon,
|
||||
iconRight: true,
|
||||
addContainer: attrs.yours,
|
||||
addContainer,
|
||||
titleOptions: { count: attrs.liked ? count - 1 : count }
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import PostCooked from "discourse/widgets/post-cooked";
|
||||
import DecoratorHelper from "discourse/widgets/decorator-helper";
|
||||
import { createWidget, applyDecorators } from "discourse/widgets/widget";
|
||||
import RawHtml from "discourse/widgets/raw-html";
|
||||
import { iconNode } from "discourse-common/lib/icon-library";
|
||||
import { transformBasicPost } from "discourse/lib/transform-post";
|
||||
import { postTransformCallbacks } from "discourse/widgets/post-stream";
|
||||
@ -459,20 +460,23 @@ createWidget("post-notice", {
|
||||
let text, icon;
|
||||
if (attrs.noticeType === "custom") {
|
||||
icon = "user-shield";
|
||||
text = attrs.noticeMessage;
|
||||
text = new RawHtml({ html: `<div>${attrs.noticeMessage}</div>` });
|
||||
} else if (attrs.noticeType === "new_user") {
|
||||
icon = "hands-helping";
|
||||
text = I18n.t("post.notice.new_user", { user });
|
||||
text = h("p", I18n.t("post.notice.new_user", { user }));
|
||||
} else if (attrs.noticeType === "returning_user") {
|
||||
icon = "far-smile";
|
||||
const distance = (new Date() - new Date(attrs.noticeTime)) / 1000;
|
||||
text = I18n.t("post.notice.returning_user", {
|
||||
user,
|
||||
time: durationTiny(distance, { addAgo: true })
|
||||
});
|
||||
text = h(
|
||||
"p",
|
||||
I18n.t("post.notice.returning_user", {
|
||||
user,
|
||||
time: durationTiny(distance, { addAgo: true })
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return h("p", [iconNode(icon), text]);
|
||||
return [iconNode(icon), text];
|
||||
}
|
||||
});
|
||||
|
||||
@ -528,7 +532,9 @@ createWidget("post-article", {
|
||||
},
|
||||
|
||||
html(attrs, state) {
|
||||
const rows = [h("a.tabLoc", { attributes: { href: "" } })];
|
||||
const rows = [
|
||||
h("a.tabLoc", { attributes: { href: "", "aria-hidden": true } })
|
||||
];
|
||||
if (state.repliesAbove.length) {
|
||||
const replies = state.repliesAbove.map(p => {
|
||||
return this.attach("embedded-post", p, {
|
||||
|
||||
@ -73,9 +73,7 @@ createSearchResult({
|
||||
return h(
|
||||
"span",
|
||||
{
|
||||
className: `tag-${tag} discourse-tag ${
|
||||
Discourse.SiteSettings.tag_style
|
||||
}`
|
||||
className: `tag-${tag} discourse-tag ${Discourse.SiteSettings.tag_style}`
|
||||
},
|
||||
tag
|
||||
);
|
||||
@ -149,13 +147,27 @@ createSearchResult({
|
||||
linkField: "url",
|
||||
builder(result, term) {
|
||||
const topic = result.topic;
|
||||
const link = h("span.topic", [
|
||||
|
||||
const firstLine = [
|
||||
this.attach("topic-status", { topic, disableActions: true }),
|
||||
h("span.topic-title", new Highlighted(topic.get("fancyTitle"), term)),
|
||||
h("span.topic-title", new Highlighted(topic.fancyTitle, term))
|
||||
];
|
||||
|
||||
const secondLine = [
|
||||
this.attach("category-link", {
|
||||
category: topic.get("category"),
|
||||
category: topic.category,
|
||||
link: false
|
||||
})
|
||||
];
|
||||
if (Discourse.SiteSettings.tagging_enabled) {
|
||||
secondLine.push(
|
||||
this.attach("discourse-tags", { topic, tagName: "span" })
|
||||
);
|
||||
}
|
||||
|
||||
const link = h("span.topic", [
|
||||
h("div.first-line", firstLine),
|
||||
h("div.second-line", secondLine)
|
||||
]);
|
||||
|
||||
return postResult.call(this, result, link, term);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user