Version bump
This commit is contained in:
commit
93e91879c7
8
Gemfile
8
Gemfile
@ -175,8 +175,16 @@ group :development do
|
||||
gem 'better_errors', platform: :mri, require: !!ENV['BETTER_ERRORS']
|
||||
gem 'binding_of_caller'
|
||||
gem 'yaml-lint'
|
||||
end
|
||||
|
||||
if ENV["ALLOW_DEV_POPULATE"] == "1"
|
||||
gem 'discourse_dev_assets'
|
||||
gem 'faker', "~> 2.16"
|
||||
else
|
||||
group :development do
|
||||
gem 'discourse_dev_assets'
|
||||
gem 'faker', "~> 2.16"
|
||||
end
|
||||
end
|
||||
|
||||
# this is an optional gem, it provides a high performance replacement
|
||||
|
||||
@ -129,7 +129,7 @@ GEM
|
||||
sprockets (>= 3.3, < 4.1)
|
||||
ember-source (2.18.2)
|
||||
erubi (1.10.0)
|
||||
excon (0.84.0)
|
||||
excon (0.85.0)
|
||||
execjs (2.8.1)
|
||||
exifr (1.3.9)
|
||||
fabrication (2.22.0)
|
||||
@ -200,7 +200,7 @@ GEM
|
||||
libv8-node (15.14.0.1-x86_64-darwin-19)
|
||||
libv8-node (15.14.0.1-x86_64-darwin-20)
|
||||
libv8-node (15.14.0.1-x86_64-linux)
|
||||
listen (3.5.1)
|
||||
listen (3.6.0)
|
||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||
rb-inotify (~> 0.9, >= 0.9.10)
|
||||
literate_randomizer (0.4.0)
|
||||
@ -407,7 +407,7 @@ GEM
|
||||
ruby-readability (0.7.0)
|
||||
guess_html_encoding (>= 0.0.4)
|
||||
nokogiri (>= 1.6.0)
|
||||
ruby2_keywords (0.0.4)
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (2.3.2)
|
||||
sanitize (5.2.3)
|
||||
crass (~> 1.0.2)
|
||||
|
||||
@ -66,7 +66,7 @@ Discourse supports the **latest, stable releases** of all major browsers and pla
|
||||
- [Redis](https://redis.io/) — We use Redis as a cache and for transient data.
|
||||
- [BrowserStack](https://www.browserstack.com/) — We use BrowserStack to test on real devices and browsers.
|
||||
|
||||
Plus *lots* of Ruby Gems, a complete list of which is at [/master/Gemfile](https://github.com/discourse/discourse/blob/master/Gemfile).
|
||||
Plus *lots* of Ruby Gems, a complete list of which is at [/main/Gemfile](https://github.com/discourse/discourse/blob/main/Gemfile).
|
||||
|
||||
## Contributing
|
||||
|
||||
@ -80,7 +80,7 @@ Before contributing to Discourse:
|
||||
1. Please read the complete mission statements on [**discourse.org**](https://www.discourse.org). Yes we actually believe this stuff; you should too.
|
||||
2. Read and sign the [**Electronic Discourse Forums Contribution License Agreement**](https://www.discourse.org/cla).
|
||||
3. Dig into [**CONTRIBUTING.MD**](CONTRIBUTING.md), which covers submitting bugs, requesting new features, preparing your code for a pull request, etc.
|
||||
4. Always strive to collaborate [with mutual respect](https://github.com/discourse/discourse/blob/master/docs/code-of-conduct.md).
|
||||
4. Always strive to collaborate [with mutual respect](https://github.com/discourse/discourse/blob/main/docs/code-of-conduct.md).
|
||||
5. Not sure what to work on? [**We've got some ideas.**](https://meta.discourse.org/t/so-you-want-to-help-out-with-discourse/3823)
|
||||
|
||||
|
||||
@ -88,7 +88,7 @@ We look forward to seeing your pull requests!
|
||||
|
||||
## Security
|
||||
|
||||
We take security very seriously at Discourse; all our code is 100% open source and peer reviewed. Please read [our security guide](https://github.com/discourse/discourse/blob/master/docs/SECURITY.md) for an overview of security measures in Discourse, or if you wish to report a security issue.
|
||||
We take security very seriously at Discourse; all our code is 100% open source and peer reviewed. Please read [our security guide](https://github.com/discourse/discourse/blob/main/docs/SECURITY.md) for an overview of security measures in Discourse, or if you wish to report a security issue.
|
||||
|
||||
## The Discourse Team
|
||||
|
||||
|
||||
@ -22,9 +22,21 @@ export default Component.extend({
|
||||
return this.onlyHex ? 6 : null;
|
||||
}),
|
||||
|
||||
normalize(color) {
|
||||
if (/^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(color)) {
|
||||
if (!color.startsWith("#")) {
|
||||
color = "#" + color;
|
||||
}
|
||||
}
|
||||
|
||||
return color;
|
||||
},
|
||||
|
||||
@action
|
||||
onHexInput(color) {
|
||||
this.attrs.onChangeColor && this.attrs.onChangeColor(color || "");
|
||||
if (this.attrs.onChangeColor) {
|
||||
this.attrs.onChangeColor(this.normalize(color || ""));
|
||||
}
|
||||
},
|
||||
|
||||
@observes("hexValue", "brightnessValue", "valid")
|
||||
@ -32,7 +44,9 @@ export default Component.extend({
|
||||
const hex = this.hexValue;
|
||||
let text = this.element.querySelector("input.hex-input");
|
||||
|
||||
this.attrs.onChangeColor && this.attrs.onChangeColor(hex);
|
||||
if (this.attrs.onChangeColor) {
|
||||
this.attrs.onChangeColor(this.normalize(hex));
|
||||
}
|
||||
|
||||
if (this.valid) {
|
||||
this.styleSelection &&
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import { COMPONENTS, THEMES } from "admin/models/theme";
|
||||
import { equal, gt } from "@ember/object/computed";
|
||||
import { equal, gt, gte } from "@ember/object/computed";
|
||||
import Component from "@ember/component";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { action } from "@ember/object";
|
||||
|
||||
export default Component.extend({
|
||||
router: service(),
|
||||
@ -10,10 +11,12 @@ export default Component.extend({
|
||||
COMPONENTS,
|
||||
|
||||
classNames: ["themes-list"],
|
||||
filterTerm: null,
|
||||
|
||||
hasThemes: gt("themesList.length", 0),
|
||||
hasActiveThemes: gt("activeThemes.length", 0),
|
||||
hasInactiveThemes: gt("inactiveThemes.length", 0),
|
||||
showFilter: gte("themesList.length", 10),
|
||||
|
||||
themesTabActive: equal("currentTab", THEMES),
|
||||
componentsTabActive: equal("currentTab", COMPONENTS),
|
||||
@ -31,28 +34,36 @@ export default Component.extend({
|
||||
"themesList",
|
||||
"currentTab",
|
||||
"themesList.@each.user_selectable",
|
||||
"themesList.@each.default"
|
||||
"themesList.@each.default",
|
||||
"filterTerm"
|
||||
)
|
||||
inactiveThemes(themes) {
|
||||
let results;
|
||||
if (this.componentsTabActive) {
|
||||
return themes.filter((theme) => theme.get("parent_themes.length") <= 0);
|
||||
results = themes.filter(
|
||||
(theme) => theme.get("parent_themes.length") <= 0
|
||||
);
|
||||
} else {
|
||||
results = themes.filter(
|
||||
(theme) => !theme.get("user_selectable") && !theme.get("default")
|
||||
);
|
||||
}
|
||||
return themes.filter(
|
||||
(theme) => !theme.get("user_selectable") && !theme.get("default")
|
||||
);
|
||||
return this._filterThemes(results, this.filterTerm);
|
||||
},
|
||||
|
||||
@discourseComputed(
|
||||
"themesList",
|
||||
"currentTab",
|
||||
"themesList.@each.user_selectable",
|
||||
"themesList.@each.default"
|
||||
"themesList.@each.default",
|
||||
"filterTerm"
|
||||
)
|
||||
activeThemes(themes) {
|
||||
let results;
|
||||
if (this.componentsTabActive) {
|
||||
return themes.filter((theme) => theme.get("parent_themes.length") > 0);
|
||||
results = themes.filter((theme) => theme.get("parent_themes.length") > 0);
|
||||
} else {
|
||||
return themes
|
||||
results = themes
|
||||
.filter((theme) => theme.get("user_selectable") || theme.get("default"))
|
||||
.sort((a, b) => {
|
||||
if (a.get("default") && !b.get("default")) {
|
||||
@ -66,16 +77,29 @@ export default Component.extend({
|
||||
.localeCompare(b.get("name").toLowerCase());
|
||||
});
|
||||
}
|
||||
return this._filterThemes(results, this.filterTerm);
|
||||
},
|
||||
|
||||
actions: {
|
||||
changeView(newTab) {
|
||||
if (newTab !== this.currentTab) {
|
||||
this.set("currentTab", newTab);
|
||||
_filterThemes(themes, term) {
|
||||
term = term?.trim()?.toLowerCase();
|
||||
if (!term) {
|
||||
return themes;
|
||||
}
|
||||
return themes.filter(({ name }) => name.toLowerCase().includes(term));
|
||||
},
|
||||
|
||||
@action
|
||||
changeView(newTab) {
|
||||
if (newTab !== this.currentTab) {
|
||||
this.set("currentTab", newTab);
|
||||
if (!this.showFilter) {
|
||||
this.set("filterTerm", null);
|
||||
}
|
||||
},
|
||||
navigateToTheme(theme) {
|
||||
this.router.transitionTo("adminCustomizeThemes.show", theme);
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
@action
|
||||
navigateToTheme(theme) {
|
||||
this.router.transitionTo("adminCustomizeThemes.show", theme);
|
||||
},
|
||||
});
|
||||
|
||||
@ -15,6 +15,17 @@
|
||||
</div>
|
||||
|
||||
<div class="themes-list-container">
|
||||
{{#if showFilter}}
|
||||
<div class="themes-list-filter themes-list-item">
|
||||
{{input
|
||||
class="filter-input"
|
||||
placeholder=(i18n "admin.customize.theme.filter_placeholder")
|
||||
autocomplete="discourse"
|
||||
value=(mut filterTerm)
|
||||
}}
|
||||
{{d-icon "search"}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{#if hasThemes}}
|
||||
{{#if hasActiveThemes}}
|
||||
{{#each activeThemes as |theme|}}
|
||||
|
||||
@ -652,7 +652,6 @@ export default Component.extend({
|
||||
this.setProperties({
|
||||
uploadProgress: 0,
|
||||
isUploading: false,
|
||||
isProcessingUpload: false,
|
||||
isCancellable: false,
|
||||
});
|
||||
}
|
||||
@ -683,6 +682,14 @@ export default Component.extend({
|
||||
});
|
||||
|
||||
$element
|
||||
.on("fileuploadprocessstart", () => {
|
||||
this.setProperties({
|
||||
uploadProgress: 0,
|
||||
isUploading: true,
|
||||
isProcessingUpload: true,
|
||||
isCancellable: false,
|
||||
});
|
||||
})
|
||||
.on("fileuploadprocess", (e, data) => {
|
||||
this.appEvents.trigger(
|
||||
"composer:insert-text",
|
||||
@ -690,12 +697,6 @@ export default Component.extend({
|
||||
filename: data.files[data.index].name,
|
||||
})}]()\n`
|
||||
);
|
||||
this.setProperties({
|
||||
uploadProgress: 0,
|
||||
isUploading: true,
|
||||
isProcessingUpload: true,
|
||||
isCancellable: false,
|
||||
});
|
||||
})
|
||||
.on("fileuploadprocessalways", (e, data) => {
|
||||
this.appEvents.trigger(
|
||||
@ -705,6 +706,8 @@ export default Component.extend({
|
||||
})}]()\n`,
|
||||
""
|
||||
);
|
||||
})
|
||||
.on("fileuploadprocessstop", () => {
|
||||
this.setProperties({
|
||||
uploadProgress: 0,
|
||||
isUploading: false,
|
||||
|
||||
@ -79,7 +79,7 @@ export default Component.extend({
|
||||
}
|
||||
},
|
||||
|
||||
willDestoryElement() {
|
||||
willDestroyElement() {
|
||||
this._tableContainer.removeEventListener("scroll", this.onBottomScroll);
|
||||
this._topHorizontalScrollBar.removeEventListener(
|
||||
"scroll",
|
||||
|
||||
@ -138,4 +138,10 @@ export default buildCategoryPanel("settings", {
|
||||
let hours = minutes ? minutes / 60 : null;
|
||||
this.set("category.auto_close_hours", hours);
|
||||
},
|
||||
|
||||
@action
|
||||
onDefaultSlowModeDurationChange(minutes) {
|
||||
let seconds = minutes ? minutes * 60 : null;
|
||||
this.set("category.default_slow_mode_seconds", seconds);
|
||||
},
|
||||
});
|
||||
|
||||
@ -37,7 +37,7 @@ export default Component.extend({
|
||||
removeFromGroup() {
|
||||
const model = this.model;
|
||||
model
|
||||
.removeMember(this.currentUser)
|
||||
.leave()
|
||||
.then(() => {
|
||||
model.set("is_group_user", false);
|
||||
this.appEvents.trigger("group:leave", model);
|
||||
@ -50,13 +50,13 @@ export default Component.extend({
|
||||
joinGroup() {
|
||||
if (this.currentUser) {
|
||||
this.set("updatingMembership", true);
|
||||
const model = this.model;
|
||||
const group = this.model;
|
||||
|
||||
model
|
||||
.addMembers(this.currentUser.get("username"))
|
||||
group
|
||||
.join()
|
||||
.then(() => {
|
||||
model.set("is_group_user", true);
|
||||
this.appEvents.trigger("group:join", model);
|
||||
group.set("is_group_user", true);
|
||||
this.appEvents.trigger("group:join", group);
|
||||
})
|
||||
.catch(popupAjaxError)
|
||||
.finally(() => {
|
||||
|
||||
@ -57,52 +57,50 @@ export default Component.extend({
|
||||
);
|
||||
}
|
||||
|
||||
this.checkGroupName();
|
||||
this.checkGroupNameDebounced();
|
||||
|
||||
return this._failedInputValidation(
|
||||
I18n.t("admin.groups.new.name.checking")
|
||||
);
|
||||
},
|
||||
|
||||
checkGroupName() {
|
||||
discourseDebounce(
|
||||
this,
|
||||
function () {
|
||||
if (isEmpty(this.nameInput)) {
|
||||
return;
|
||||
checkGroupNameDebounced() {
|
||||
discourseDebounce(this, this._checkGroupName, 500);
|
||||
},
|
||||
|
||||
_checkGroupName() {
|
||||
if (isEmpty(this.nameInput)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Group.checkName(this.nameInput)
|
||||
.then((response) => {
|
||||
const validationName = "uniqueNameValidation";
|
||||
|
||||
if (response.available) {
|
||||
this.set(
|
||||
validationName,
|
||||
EmberObject.create({
|
||||
ok: true,
|
||||
reason: I18n.t("admin.groups.new.name.available"),
|
||||
})
|
||||
);
|
||||
|
||||
this.set("disableSave", false);
|
||||
this.set("model.name", this.nameInput);
|
||||
} else {
|
||||
let reason;
|
||||
|
||||
if (response.errors) {
|
||||
reason = response.errors.join(" ");
|
||||
} else {
|
||||
reason = I18n.t("admin.groups.new.name.not_available");
|
||||
}
|
||||
|
||||
this.set(validationName, this._failedInputValidation(reason));
|
||||
}
|
||||
|
||||
Group.checkName(this.nameInput)
|
||||
.then((response) => {
|
||||
const validationName = "uniqueNameValidation";
|
||||
|
||||
if (response.available) {
|
||||
this.set(
|
||||
validationName,
|
||||
EmberObject.create({
|
||||
ok: true,
|
||||
reason: I18n.t("admin.groups.new.name.available"),
|
||||
})
|
||||
);
|
||||
|
||||
this.set("disableSave", false);
|
||||
this.set("model.name", this.nameInput);
|
||||
} else {
|
||||
let reason;
|
||||
|
||||
if (response.errors) {
|
||||
reason = response.errors.join(" ");
|
||||
} else {
|
||||
reason = I18n.t("admin.groups.new.name.not_available");
|
||||
}
|
||||
|
||||
this.set(validationName, this._failedInputValidation(reason));
|
||||
}
|
||||
})
|
||||
.catch(popupAjaxError);
|
||||
},
|
||||
500
|
||||
);
|
||||
})
|
||||
.catch(popupAjaxError);
|
||||
},
|
||||
|
||||
_failedInputValidation(reason) {
|
||||
|
||||
@ -0,0 +1,106 @@
|
||||
import Component from "@ember/component";
|
||||
import { action } from "@ember/object";
|
||||
import { empty } from "@ember/object/computed";
|
||||
import { bind, default as computed } from "discourse-common/utils/decorators";
|
||||
import I18n from "I18n";
|
||||
|
||||
export default Component.extend({
|
||||
classNames: ["pick-files-button"],
|
||||
acceptedFileTypes: null,
|
||||
acceptAnyFile: empty("acceptedFileTypes"),
|
||||
|
||||
didInsertElement() {
|
||||
this._super(...arguments);
|
||||
const fileInput = this.element.querySelector("input");
|
||||
this.set("fileInput", fileInput);
|
||||
fileInput.addEventListener("change", this.onChange, false);
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
this._super(...arguments);
|
||||
this.fileInput.removeEventListener("change", this.onChange);
|
||||
},
|
||||
|
||||
@bind
|
||||
onChange() {
|
||||
const files = this.fileInput.files;
|
||||
this._filesPicked(files);
|
||||
},
|
||||
|
||||
@computed
|
||||
acceptedFileTypesString() {
|
||||
if (!this.acceptedFileTypes) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.acceptedFileTypes.join(",");
|
||||
},
|
||||
|
||||
@computed
|
||||
acceptedExtensions() {
|
||||
if (!this.acceptedFileTypes) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.acceptedFileTypes
|
||||
.filter((type) => type.startsWith("."))
|
||||
.map((type) => type.substring(1));
|
||||
},
|
||||
|
||||
@computed
|
||||
acceptedMimeTypes() {
|
||||
if (!this.acceptedFileTypes) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.acceptedFileTypes.filter((type) => !type.startsWith("."));
|
||||
},
|
||||
|
||||
@action
|
||||
openSystemFilePicker() {
|
||||
this.fileInput.click();
|
||||
},
|
||||
|
||||
_filesPicked(files) {
|
||||
if (!files || !files.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._haveAcceptedTypes(files)) {
|
||||
const message = I18n.t("pick_files_button.unsupported_file_picked", {
|
||||
types: this.acceptedFileTypesString,
|
||||
});
|
||||
bootbox.alert(message);
|
||||
return;
|
||||
}
|
||||
this.onFilesPicked(files);
|
||||
},
|
||||
|
||||
_haveAcceptedTypes(files) {
|
||||
for (const file of files) {
|
||||
if (
|
||||
!(this._hasAcceptedExtension(file) && this._hasAcceptedMimeType(file))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
_hasAcceptedExtension(file) {
|
||||
const extension = this._fileExtension(file.name);
|
||||
return (
|
||||
!this.acceptedExtensions || this.acceptedExtensions.includes(extension)
|
||||
);
|
||||
},
|
||||
|
||||
_hasAcceptedMimeType(file) {
|
||||
return (
|
||||
!this.acceptedMimeTypes || this.acceptedMimeTypes.includes(file.type)
|
||||
);
|
||||
},
|
||||
|
||||
_fileExtension(fileName) {
|
||||
return fileName.split(".").pop();
|
||||
},
|
||||
});
|
||||
@ -29,14 +29,14 @@ export default Component.extend({
|
||||
|
||||
@discourseComputed(
|
||||
"reviewable.type",
|
||||
"reviewable.stale",
|
||||
"reviewable.last_performing_username",
|
||||
"siteSettings.blur_tl0_flagged_posts_media",
|
||||
"reviewable.target_created_by_trust_level"
|
||||
)
|
||||
customClasses(type, stale, blurEnabled, trustLevel) {
|
||||
customClasses(type, lastPerformingUsername, blurEnabled, trustLevel) {
|
||||
let classes = type.dasherize();
|
||||
|
||||
if (stale) {
|
||||
if (lastPerformingUsername) {
|
||||
classes = `${classes} reviewable-stale`;
|
||||
}
|
||||
|
||||
|
||||
@ -49,8 +49,11 @@ export default Component.extend({
|
||||
if (this.element) {
|
||||
const linkInput = this.element.querySelector("#share-link input");
|
||||
linkInput.value = this.link;
|
||||
linkInput.setSelectionRange(0, this.link.length);
|
||||
linkInput.focus();
|
||||
if (!this.site.mobileView) {
|
||||
// if the input is auto-focused on mobile, iOS requires two taps of the copy button
|
||||
linkInput.setSelectionRange(0, this.link.length);
|
||||
linkInput.focus();
|
||||
}
|
||||
}
|
||||
}, 200);
|
||||
},
|
||||
|
||||
@ -218,7 +218,6 @@ const SiteHeaderComponent = MountWidget.extend(
|
||||
|
||||
this.dispatch("notifications:changed", "user-notifications");
|
||||
this.dispatch("header:keyboard-trigger", "header");
|
||||
this.dispatch("search-autocomplete:after-complete", "search-term");
|
||||
this.dispatch("user-menu:navigation", "user-menu");
|
||||
|
||||
this.appEvents.on("dom:clean", this, "_cleanDom");
|
||||
|
||||
@ -5,7 +5,7 @@ import PanEvents, {
|
||||
import Component from "@ember/component";
|
||||
import EmberObject from "@ember/object";
|
||||
import discourseDebounce from "discourse-common/lib/debounce";
|
||||
import { later } from "@ember/runloop";
|
||||
import { later, next } from "@ember/runloop";
|
||||
import { observes } from "discourse-common/utils/decorators";
|
||||
import showModal from "discourse/lib/show-modal";
|
||||
|
||||
@ -16,12 +16,23 @@ export default Component.extend(PanEvents, {
|
||||
composerOpen: null,
|
||||
info: null,
|
||||
isPanning: false,
|
||||
canRender: true,
|
||||
_lastTopicId: null,
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.set("info", EmberObject.create());
|
||||
},
|
||||
|
||||
didUpdateAttrs() {
|
||||
this._super(...arguments);
|
||||
if (this._lastTopicId !== this.topic.id) {
|
||||
this._lastTopicId = this.topic.id;
|
||||
this.set("canRender", false);
|
||||
next(() => this.set("canRender", true));
|
||||
}
|
||||
},
|
||||
|
||||
_performCheckSize() {
|
||||
if (!this.element || this.isDestroying || this.isDestroyed) {
|
||||
return;
|
||||
@ -180,6 +191,8 @@ export default Component.extend(PanEvents, {
|
||||
didInsertElement() {
|
||||
this._super(...arguments);
|
||||
|
||||
this._lastTopicId = this.topic.id;
|
||||
|
||||
this.appEvents
|
||||
.on("topic:current-post-scrolled", this, this._topicScrolled)
|
||||
.on("topic:jump-to-post", this, this._collapseFullscreen)
|
||||
|
||||
@ -11,9 +11,8 @@ export default Component.extend({
|
||||
if (this.canAct && $(e.target).hasClass("d-icon-thumbtack")) {
|
||||
const topic = this.topic;
|
||||
topic.get("pinned") ? topic.clearPin() : topic.rePin();
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
@discourseComputed("disableActions")
|
||||
|
||||
@ -663,7 +663,7 @@ export default Controller.extend({
|
||||
},
|
||||
},
|
||||
|
||||
disableSubmit: or("model.loading", "isUploading"),
|
||||
disableSubmit: or("model.loading", "isUploading", "isProcessingUpload"),
|
||||
|
||||
save(force, options = {}) {
|
||||
if (this.disableSubmit) {
|
||||
|
||||
@ -7,10 +7,10 @@ import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
export default Controller.extend({
|
||||
application: controller(),
|
||||
|
||||
queryParams: ["order", "desc", "filter"],
|
||||
queryParams: ["order", "asc", "filter"],
|
||||
|
||||
order: "",
|
||||
desc: null,
|
||||
asc: null,
|
||||
filter: null,
|
||||
filterInput: null,
|
||||
|
||||
@ -27,7 +27,7 @@ export default Controller.extend({
|
||||
);
|
||||
},
|
||||
|
||||
@observes("order", "desc", "filter")
|
||||
@observes("order", "asc", "filter")
|
||||
_filtersChanged() {
|
||||
this.findRequesters(true);
|
||||
},
|
||||
@ -57,9 +57,9 @@ export default Controller.extend({
|
||||
});
|
||||
},
|
||||
|
||||
@discourseComputed("order", "desc", "filter")
|
||||
memberParams(order, desc, filter) {
|
||||
return { order, desc, filter };
|
||||
@discourseComputed("order", "asc", "filter")
|
||||
memberParams(order, asc, filter) {
|
||||
return { order, asc, filter };
|
||||
},
|
||||
|
||||
@discourseComputed("model.requesters.[]")
|
||||
|
||||
@ -31,6 +31,7 @@ export default Controller.extend(
|
||||
accountEmail: alias("email"),
|
||||
hiddenEmail: alias("model.hidden_email"),
|
||||
emailVerifiedByLink: alias("model.email_verified_by_link"),
|
||||
differentExternalEmail: alias("model.different_external_email"),
|
||||
accountUsername: alias("model.username"),
|
||||
passwordRequired: notEmpty("accountPassword"),
|
||||
successMessage: null,
|
||||
@ -130,7 +131,8 @@ export default Controller.extend(
|
||||
"authOptions.email",
|
||||
"authOptions.email_valid",
|
||||
"hiddenEmail",
|
||||
"emailVerifiedByLink"
|
||||
"emailVerifiedByLink",
|
||||
"differentExternalEmail"
|
||||
)
|
||||
emailValidation(
|
||||
email,
|
||||
@ -138,9 +140,10 @@ export default Controller.extend(
|
||||
externalAuthEmail,
|
||||
externalAuthEmailValid,
|
||||
hiddenEmail,
|
||||
emailVerifiedByLink
|
||||
emailVerifiedByLink,
|
||||
differentExternalEmail
|
||||
) {
|
||||
if (hiddenEmail) {
|
||||
if (hiddenEmail && !differentExternalEmail) {
|
||||
return EmberObject.create({
|
||||
ok: true,
|
||||
reason: I18n.t("user.email.ok"),
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
import Controller from "@ember/controller";
|
||||
|
||||
export default Controller.extend({});
|
||||
|
||||
@ -60,12 +60,22 @@ export default {
|
||||
// Refresh if necessary
|
||||
document.location.reload(true);
|
||||
} else if (me.new_href && me.target) {
|
||||
const link_target = me.theme_id
|
||||
? `[data-target="${me.target}"][data-theme-id="${me.theme_id}"]`
|
||||
: `[data-target="${me.target}"]`;
|
||||
document.querySelectorAll(`link${link_target}`).forEach((link) => {
|
||||
this.refreshCSS(link, me.new_href);
|
||||
});
|
||||
const link_target = !!me.theme_id
|
||||
? `[data-target='${me.target}'][data-theme-id='${me.theme_id}']`
|
||||
: `[data-target='${me.target}']`;
|
||||
|
||||
const links = document.querySelectorAll(`link${link_target}`);
|
||||
if (links.length > 0) {
|
||||
const lastLink = links[links.length - 1];
|
||||
// this check is useful when message-bus has multiple file updates
|
||||
// it avoids the browser doing a lot of work for nothing
|
||||
// should the filenames be unchanged
|
||||
if (
|
||||
lastLink.href.split("/").pop() !== me.new_href.split("/").pop()
|
||||
) {
|
||||
this.refreshCSS(lastLink, me.new_href);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
@ -74,21 +84,14 @@ export default {
|
||||
},
|
||||
|
||||
refreshCSS(node, newHref) {
|
||||
if (node.dataset.reloading) {
|
||||
clearTimeout(node.dataset.timeout);
|
||||
}
|
||||
|
||||
node.dataset.reloading = true;
|
||||
|
||||
let reloaded = node.cloneNode(true);
|
||||
reloaded.href = newHref;
|
||||
node.insertAdjacentElement("afterend", reloaded);
|
||||
|
||||
let timeout = setTimeout(() => {
|
||||
node.parentNode.removeChild(node);
|
||||
reloaded.dataset.reloading = false;
|
||||
}, 2000);
|
||||
|
||||
node.dataset.timeout = timeout;
|
||||
setTimeout(() => {
|
||||
if (node && node.parentNode) {
|
||||
node.parentNode.removeChild(node);
|
||||
}
|
||||
}, 500);
|
||||
},
|
||||
};
|
||||
|
||||
@ -86,7 +86,8 @@ export default {
|
||||
messageBus.baseUrl =
|
||||
siteSettings.long_polling_base_url.replace(/\/$/, "") + "/";
|
||||
|
||||
messageBus.enableChunkedEncoding = siteSettings.enable_chunked_encoding;
|
||||
messageBus.enableChunkedEncoding =
|
||||
isProduction() && siteSettings.enable_chunked_encoding;
|
||||
|
||||
if (messageBus.baseUrl !== "/") {
|
||||
messageBus.ajax = function (opts) {
|
||||
|
||||
@ -73,9 +73,10 @@ import { replaceFormatter } from "discourse/lib/utilities";
|
||||
import { replaceTagRenderer } from "discourse/lib/render-tag";
|
||||
import { setNewCategoryDefaultColors } from "discourse/routes/new-category";
|
||||
import { addSearchResultsCallback } from "discourse/lib/search";
|
||||
import { addSearchSuggestion } from "discourse/widgets/search-menu-results";
|
||||
|
||||
// If you add any methods to the API ensure you bump up this number
|
||||
const PLUGIN_API_VERSION = "0.11.5";
|
||||
const PLUGIN_API_VERSION = "0.11.7";
|
||||
|
||||
class PluginApi {
|
||||
constructor(version, container) {
|
||||
@ -1295,6 +1296,18 @@ class PluginApi {
|
||||
addSearchResultsCallback(callback) {
|
||||
addSearchResultsCallback(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a suggestion shortcut to search menu panel.
|
||||
*
|
||||
* ```
|
||||
* addSearchSuggestion("in:assigned");
|
||||
* ```
|
||||
*
|
||||
*/
|
||||
addSearchSuggestion(value) {
|
||||
addSearchSuggestion(value);
|
||||
}
|
||||
}
|
||||
|
||||
// from http://stackoverflow.com/questions/6832596/how-to-compare-software-version-number-using-js-only-number
|
||||
|
||||
@ -206,53 +206,30 @@ export function isValidSearchTerm(searchTerm, siteSettings) {
|
||||
}
|
||||
}
|
||||
|
||||
export function applySearchAutocomplete(
|
||||
$input,
|
||||
siteSettings,
|
||||
appEvents,
|
||||
options
|
||||
) {
|
||||
const afterComplete = function () {
|
||||
if (appEvents) {
|
||||
appEvents.trigger("search-autocomplete:after-complete");
|
||||
}
|
||||
};
|
||||
|
||||
export function applySearchAutocomplete($input, siteSettings) {
|
||||
$input.autocomplete(
|
||||
deepMerge(
|
||||
{
|
||||
template: findRawTemplate("category-tag-autocomplete"),
|
||||
key: "#",
|
||||
width: "100%",
|
||||
treatAsTextarea: true,
|
||||
autoSelectFirstSuggestion: false,
|
||||
transformComplete(obj) {
|
||||
return obj.text;
|
||||
},
|
||||
dataSource(term) {
|
||||
return searchCategoryTag(term, siteSettings);
|
||||
},
|
||||
afterComplete,
|
||||
},
|
||||
options
|
||||
)
|
||||
deepMerge({
|
||||
template: findRawTemplate("category-tag-autocomplete"),
|
||||
key: "#",
|
||||
width: "100%",
|
||||
treatAsTextarea: true,
|
||||
autoSelectFirstSuggestion: false,
|
||||
transformComplete: (obj) => obj.text,
|
||||
dataSource: (term) => searchCategoryTag(term, siteSettings),
|
||||
})
|
||||
);
|
||||
|
||||
if (siteSettings.enable_mentions) {
|
||||
$input.autocomplete(
|
||||
deepMerge(
|
||||
{
|
||||
template: findRawTemplate("user-selector-autocomplete"),
|
||||
key: "@",
|
||||
width: "100%",
|
||||
treatAsTextarea: true,
|
||||
autoSelectFirstSuggestion: false,
|
||||
transformComplete: (v) => v.username || v.name,
|
||||
dataSource: (term) => userSearch({ term, includeGroups: true }),
|
||||
afterComplete,
|
||||
},
|
||||
options
|
||||
)
|
||||
deepMerge({
|
||||
template: findRawTemplate("user-selector-autocomplete"),
|
||||
key: "@",
|
||||
width: "100%",
|
||||
treatAsTextarea: true,
|
||||
autoSelectFirstSuggestion: false,
|
||||
transformComplete: (v) => v.username || v.name,
|
||||
dataSource: (term) => userSearch({ term, includeGroups: true }),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,6 +22,8 @@ function performSearch(
|
||||
allowedUsers,
|
||||
groupMembersOf,
|
||||
includeStagedUsers,
|
||||
lastSeenUsers,
|
||||
limit,
|
||||
resultsFn
|
||||
) {
|
||||
let cached = cache[term];
|
||||
@ -32,7 +34,7 @@ function performSearch(
|
||||
|
||||
const eagerComplete = eagerCompleteSearch(term, topicId || categoryId);
|
||||
|
||||
if (term === "" && !eagerComplete) {
|
||||
if (term === "" && !eagerComplete && !lastSeenUsers) {
|
||||
// The server returns no results in this case, so no point checking
|
||||
// do not return empty list, because autocomplete will get terminated
|
||||
resultsFn(CANCELLED_STATUS);
|
||||
@ -51,6 +53,8 @@ function performSearch(
|
||||
groups: groupMembersOf,
|
||||
topic_allowed_users: allowedUsers,
|
||||
include_staged_users: includeStagedUsers,
|
||||
last_seen_users: lastSeenUsers,
|
||||
limit: limit,
|
||||
},
|
||||
});
|
||||
|
||||
@ -93,6 +97,8 @@ let debouncedSearch = function (
|
||||
allowedUsers,
|
||||
groupMembersOf,
|
||||
includeStagedUsers,
|
||||
lastSeenUsers,
|
||||
limit,
|
||||
resultsFn
|
||||
) {
|
||||
discourseDebounce(
|
||||
@ -107,6 +113,8 @@ let debouncedSearch = function (
|
||||
allowedUsers,
|
||||
groupMembersOf,
|
||||
includeStagedUsers,
|
||||
lastSeenUsers,
|
||||
limit,
|
||||
resultsFn,
|
||||
300
|
||||
);
|
||||
@ -169,7 +177,10 @@ function organizeResults(r, options) {
|
||||
// we also ignore if we notice a double space or a string that is only a space
|
||||
const ignoreRegex = /([\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*,\/:;<=>?\[\]^`{|}~])|\s\s|^\s$|^[^+]*\+[^@]*$/;
|
||||
|
||||
export function skipSearch(term, allowEmails) {
|
||||
export function skipSearch(term, allowEmails, lastSeenUsers = false) {
|
||||
if (lastSeenUsers) {
|
||||
return false;
|
||||
}
|
||||
if (term.indexOf("@") > -1 && !allowEmails) {
|
||||
return true;
|
||||
}
|
||||
@ -194,7 +205,9 @@ export default function userSearch(options) {
|
||||
topicId = options.topicId,
|
||||
categoryId = options.categoryId,
|
||||
groupMembersOf = options.groupMembersOf,
|
||||
includeStagedUsers = options.includeStagedUsers;
|
||||
includeStagedUsers = options.includeStagedUsers,
|
||||
lastSeenUsers = options.lastSeenUsers,
|
||||
limit = options.limit || 6;
|
||||
|
||||
if (oldSearch) {
|
||||
oldSearch.abort();
|
||||
@ -217,7 +230,7 @@ export default function userSearch(options) {
|
||||
clearPromise = later(() => resolve(CANCELLED_STATUS), 5000);
|
||||
}
|
||||
|
||||
if (skipSearch(term, options.allowEmails)) {
|
||||
if (skipSearch(term, options.allowEmails, options.lastSeenUsers)) {
|
||||
resolve([]);
|
||||
return;
|
||||
}
|
||||
@ -232,6 +245,8 @@ export default function userSearch(options) {
|
||||
allowedUsers,
|
||||
groupMembersOf,
|
||||
includeStagedUsers,
|
||||
lastSeenUsers,
|
||||
limit,
|
||||
function (r) {
|
||||
cancel(clearPromise);
|
||||
resolve(organizeResults(r, options));
|
||||
|
||||
@ -177,6 +177,11 @@ const Category = RestModel.extend({
|
||||
return topicCount;
|
||||
},
|
||||
|
||||
@discourseComputed("default_slow_mode_seconds")
|
||||
defaultSlowModeMinutes(seconds) {
|
||||
return seconds ? seconds / 60 : null;
|
||||
},
|
||||
|
||||
save() {
|
||||
const id = this.id;
|
||||
const url = id ? `/categories/${id}` : "/categories";
|
||||
@ -193,6 +198,7 @@ const Category = RestModel.extend({
|
||||
auto_close_based_on_last_post: this.get(
|
||||
"auto_close_based_on_last_post"
|
||||
),
|
||||
default_slow_mode_seconds: this.default_slow_mode_seconds,
|
||||
position: this.position,
|
||||
email_in: this.email_in,
|
||||
email_in_allow_strangers: this.email_in_allow_strangers,
|
||||
|
||||
@ -114,6 +114,12 @@ const Group = RestModel.extend({
|
||||
}).then(() => this.findMembers(params, true));
|
||||
},
|
||||
|
||||
leave() {
|
||||
return ajax(`/groups/${this.id}/leave.json`, {
|
||||
type: "DELETE",
|
||||
}).then(() => this.findMembers({}, true));
|
||||
},
|
||||
|
||||
addMembers(usernames, filter, notifyUsers, emails = []) {
|
||||
return ajax(`/groups/${this.id}/members.json`, {
|
||||
type: "PUT",
|
||||
@ -127,6 +133,14 @@ const Group = RestModel.extend({
|
||||
});
|
||||
},
|
||||
|
||||
join() {
|
||||
return ajax(`/groups/${this.id}/join.json`, {
|
||||
type: "PUT",
|
||||
}).then(() => {
|
||||
this.findMembers({}, true);
|
||||
});
|
||||
},
|
||||
|
||||
addOwners(usernames, filter, notifyUsers) {
|
||||
return ajax(`/admin/groups/${this.id}/owners.json`, {
|
||||
type: "PUT",
|
||||
|
||||
@ -22,12 +22,14 @@ const UserBadge = EmberObject.extend({
|
||||
},
|
||||
|
||||
favorite() {
|
||||
return ajax(`/user_badges/${this.id}/toggle_favorite`, { type: "PUT" })
|
||||
.then((json) => {
|
||||
this.set("is_favorite", json.user_badge.is_favorite);
|
||||
return this;
|
||||
})
|
||||
.catch(popupAjaxError);
|
||||
this.toggleProperty("is_favorite");
|
||||
return ajax(`/user_badges/${this.id}/toggle_favorite`, {
|
||||
type: "PUT",
|
||||
}).catch((e) => {
|
||||
// something went wrong, switch the UI back:
|
||||
this.toggleProperty("is_favorite");
|
||||
popupAjaxError(e);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -37,7 +37,7 @@ export const SECOND_FACTOR_METHODS = {
|
||||
SECURITY_KEY: 3,
|
||||
};
|
||||
|
||||
const isForever = (dt) => moment().diff(dt, "years") < -500;
|
||||
const isForever = (dt) => moment().diff(dt, "years") < -100;
|
||||
|
||||
let userFields = [
|
||||
"bio_raw",
|
||||
|
||||
@ -39,6 +39,8 @@ export default DiscourseRoute.extend({
|
||||
sort_order: meta.sort_order,
|
||||
additionalFilters: meta.additional_filters || {},
|
||||
});
|
||||
|
||||
controller.reviewables.setEach("last_performing_username", null);
|
||||
},
|
||||
|
||||
activate() {
|
||||
@ -62,7 +64,6 @@ export default DiscourseRoute.extend({
|
||||
const updates = data.updates[reviewable.id];
|
||||
if (updates) {
|
||||
reviewable.setProperties(updates);
|
||||
reviewable.set("stale", true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -12,6 +12,17 @@ export default DiscourseRoute.extend({
|
||||
},
|
||||
|
||||
afterModel(result) {
|
||||
DiscourseURL.routeTo(result.url, { replaceURL: true });
|
||||
// Using { replaceURL: true } to replace the current incomplete URL with
|
||||
// the complete one is working incorrectly.
|
||||
//
|
||||
// Let's consider an example where the user is at /t/-/1. If they click on
|
||||
// a link to /t/2 the expected behavior is to take the user to /t/2 that
|
||||
// will redirect to /t/-/2 and generate a history with two entries: /t/-/1
|
||||
// followed by /t/-/2.
|
||||
//
|
||||
// When { replaceURL: true } is present, the history contains a single
|
||||
// entry /t/-/2. This suggests that `afterModel` is called in the context
|
||||
// of the referrer replacing its entry with the new one.
|
||||
DiscourseURL.routeTo(result.url);
|
||||
},
|
||||
});
|
||||
|
||||
@ -43,7 +43,7 @@ export default class MediaOptimizationWorkerService extends Service {
|
||||
if (
|
||||
file.size <
|
||||
this.siteSettings
|
||||
.composer_media_optimization_image_kilobytes_optimization_threshold
|
||||
.composer_media_optimization_image_bytes_optimization_threshold
|
||||
) {
|
||||
return data;
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
{{#if hasIncoming}}
|
||||
<div class="show-mores">
|
||||
<a tabindex="0" href {{action showInserted}} class="alert alert-info clickable">
|
||||
{{count-i18n key="topic_count_" suffix="latest" count=2}}
|
||||
{{count-i18n key="topic_count_" suffix="latest" count=incomingCount}}
|
||||
</a>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
@ -116,6 +116,21 @@
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<section class="field default-slow-mode">
|
||||
<div class="control-group">
|
||||
<label for="category-default-slow-mode">
|
||||
{{i18n "category.default_slow_mode"}}
|
||||
</label>
|
||||
<div class="category-default-slow-mode-seconds">
|
||||
{{relative-time-picker
|
||||
id="category-default-slow-mode"
|
||||
durationMinutes=category.defaultSlowModeMinutes
|
||||
hiddenIntervals=hiddenRelativeIntervals
|
||||
onChange=(action "onDefaultSlowModeDurationChange")}}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="field auto-close">
|
||||
<div class="control-group">
|
||||
<label for="topic-auto-close">
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
{{d-button action=(action "openSystemFilePicker") label=label icon=icon}}
|
||||
{{#if acceptAnyFile}}
|
||||
<input type="file">
|
||||
{{else}}
|
||||
<input type="file" accept={{acceptedFileTypesString}}>
|
||||
{{/if}}
|
||||
@ -44,8 +44,8 @@
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="reviewable-actions">
|
||||
{{#if reviewable.stale}}
|
||||
<div class="stale-help">{{i18n "review.stale_help"}}</div>
|
||||
{{#if reviewable.last_performing_username}}
|
||||
<div class="stale-help">{{html-safe (i18n "review.stale_help" username=reviewable.last_performing_username)}}</div>
|
||||
{{else}}
|
||||
{{#if claimEnabled}}
|
||||
<div class="claimed-actions">
|
||||
|
||||
@ -4,13 +4,21 @@
|
||||
{{#if date}}
|
||||
<span class="date">{{displayDate}}</span>
|
||||
{{/if}}
|
||||
|
||||
{{d-button
|
||||
action=(action "close")
|
||||
class="btn btn-flat close"
|
||||
icon="times"
|
||||
aria-label="share.close"
|
||||
title="share.close"
|
||||
}}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input type="text" aria-label={{i18n "share.url"}}>
|
||||
<div class="link-share-container">
|
||||
<input class="share-link-input" type="text" aria-label={{i18n "share.url"}}> {{copy-button selector="input.share-link-input"}}
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<div class="link-share-actions">
|
||||
<div class="sources">
|
||||
{{#each sources as |s|}}
|
||||
{{share-source source=s title=model.title action=(action "share")}}
|
||||
@ -19,27 +27,26 @@
|
||||
|
||||
<div class="alt-actions">
|
||||
{{#if topic.details.can_reply_as_new_topic}}
|
||||
<div class="reply-as-new-topic">
|
||||
{{#if topic.isPrivateMessage}}
|
||||
<a href class="new-topic" {{action "replyAsNewTopic"}} aria-label={{i18n "post.reply_as_new_private_message"}} title={{i18n "post.reply_as_new_private_message"}}>
|
||||
{{d-icon "plus"}}
|
||||
{{i18n "user.new_private_message"}}
|
||||
</a>
|
||||
{{else}}
|
||||
<a href class="new-topic" {{action "replyAsNewTopic"}} aria-label={{i18n "post.reply_as_new_topic"}} title={{i18n "post.reply_as_new_topic"}}>
|
||||
{{d-icon "plus"}}
|
||||
{{i18n "topic.create"}}
|
||||
</a>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{#if topic.isPrivateMessage}}
|
||||
{{d-button
|
||||
action=(action "replyAsNewTopic")
|
||||
class="btn btn-default new-topic"
|
||||
icon="plus"
|
||||
aria-label="post.reply_as_new_private_message"
|
||||
title="post.reply_as_new_private_message"
|
||||
label="user.new_private_message"
|
||||
}}
|
||||
{{else}}
|
||||
{{d-button
|
||||
action=(action "replyAsNewTopic")
|
||||
class="btn btn-default new-topic"
|
||||
icon="plus"
|
||||
aria-label="post.reply_as_new_topic"
|
||||
title="post.reply_as_new_topic"
|
||||
label="topic.create"
|
||||
}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{d-button
|
||||
action=(action "close")
|
||||
class="btn btn-flat close"
|
||||
icon="times"
|
||||
aria-label="share.close"
|
||||
title="share.close"
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1 +1,7 @@
|
||||
{{d-button action=(action "share" source) class=(concat "share-" source.id) translatedTitle=source.title translatedAriaLabel=source.title icon=(if source.icon source.icon source.htmlIcon)}}
|
||||
{{d-button
|
||||
action=(action "share" source)
|
||||
class=(concat "btn-default share-" source.id)
|
||||
translatedTitle=source.title
|
||||
translatedAriaLabel=source.title
|
||||
icon=(if source.icon source.icon source.htmlIcon)
|
||||
}}
|
||||
|
||||
@ -1 +1,3 @@
|
||||
{{yield info}}
|
||||
{{#if canRender}}
|
||||
{{yield info}}
|
||||
{{/if}}
|
||||
|
||||
@ -30,3 +30,4 @@
|
||||
{{~#if topicInvisible~}}
|
||||
<span title={{invisibleTitle}} class="topic-status">{{invisibleIcon}}</span>
|
||||
{{~/if~}}
|
||||
{{plugin-outlet name="after-topic-status" tagName="" args=(hash topic=topic)}}
|
||||
|
||||
@ -167,7 +167,7 @@
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#if isUploading}}
|
||||
{{#if (or isUploading isProcessingUpload)}}
|
||||
<div id="file-uploading">
|
||||
{{#if isProcessingUpload}}
|
||||
{{loading-spinner size="small"}}<span>{{i18n "upload_selector.processing"}}</span>
|
||||
|
||||
@ -0,0 +1,11 @@
|
||||
<a href {{action closeMessage message}} class="close">{{d-icon "times"}}</a>
|
||||
{{html-safe message.body}}
|
||||
|
||||
{{#if currentUser.can_invite_to_forum}}
|
||||
{{d-button
|
||||
class="btn-primary"
|
||||
label="topic.invite_reply.title"
|
||||
icon="user-friends"
|
||||
action=(route-action "showInvite")
|
||||
}}
|
||||
{{/if}}
|
||||
@ -35,7 +35,7 @@
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{#conditional-loading-spinner condition=model.loading}}
|
||||
{{#conditional-loading-spinner condition=isLoading}}
|
||||
{{#if model.length}}
|
||||
<div class="total-rows">{{i18n "directory.total_rows" count=model.totalRows}}</div>
|
||||
{{#each model as |item|}}
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
<form>
|
||||
<div class="input-group invite-link">
|
||||
<label for="invite-link">{{i18n "user.invited.invite.instructions"}}</label>
|
||||
<div class="invite-input-with-button">
|
||||
<div class="link-share-container">
|
||||
{{input
|
||||
name="invite-link"
|
||||
class="invite-link"
|
||||
@ -19,7 +19,7 @@
|
||||
|
||||
<div class="input-group input-email">
|
||||
<label for="invite-email">{{d-icon "envelope"}}{{i18n "user.invited.invite.restrict_email"}}</label>
|
||||
<div class="invite-input-with-button">
|
||||
<div class="invite-email-container">
|
||||
{{input
|
||||
id="invite-email"
|
||||
value=buffered.email
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
<form>
|
||||
<div class="input-group invite-link">
|
||||
<label for="invite-link">{{i18n "topic.share.instructions"}}</label>
|
||||
<div class="invite-input-with-button">
|
||||
<div class="link-share-container">
|
||||
{{input
|
||||
name="invite-link"
|
||||
class="invite-link"
|
||||
@ -12,50 +12,51 @@
|
||||
{{copy-button selector="input.invite-link"}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="link-share-actions">
|
||||
<div class="sources">
|
||||
{{#each sources as |s|}}
|
||||
{{share-source source=s title=topic.title action=(action "share")}}
|
||||
{{/each}}
|
||||
|
||||
<div class="sources">
|
||||
{{#each sources as |s|}}
|
||||
{{share-source source=s title=topic.title action=(action "share")}}
|
||||
{{/each}}
|
||||
|
||||
{{d-button
|
||||
class="btn-default"
|
||||
label="topic.share.notify_users.title"
|
||||
icon="hand-point-right"
|
||||
action=(action "toggleNotifyUsers")
|
||||
}}
|
||||
|
||||
{{#if allowInvites}}
|
||||
{{d-button
|
||||
class="btn-default"
|
||||
label="topic.share.invite_users"
|
||||
icon="user-plus"
|
||||
action=(action "inviteUsers")
|
||||
label="topic.share.notify_users.title"
|
||||
icon="hand-point-right"
|
||||
action=(action "toggleNotifyUsers")
|
||||
}}
|
||||
|
||||
{{#if allowInvites}}
|
||||
{{d-button
|
||||
class="btn-default"
|
||||
label="topic.share.invite_users"
|
||||
icon="user-plus"
|
||||
action=(action "inviteUsers")
|
||||
}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{#if showNotifyUsers}}
|
||||
<div class="input-group invite-users">
|
||||
<label for="invite-users">{{i18n "topic.share.notify_users.instructions"}}</label>
|
||||
<div class="notify-user-input">
|
||||
{{user-chooser
|
||||
value=users
|
||||
onChange=(action "onChangeUsers")
|
||||
options=(hash
|
||||
topicId=topic.id
|
||||
maximum=(unless currentUser.staff 1)
|
||||
excludeCurrentUser=true
|
||||
)
|
||||
}}
|
||||
{{d-button
|
||||
icon="check"
|
||||
class="btn-primary"
|
||||
disabled=(if users false true)
|
||||
action=(action "notifyUsers")
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{#if showNotifyUsers}}
|
||||
<div class="input-group invite-users">
|
||||
<label for="invite-users">{{i18n "topic.share.notify_users.instructions"}}</label>
|
||||
<div class="invite-input-with-button">
|
||||
{{user-chooser
|
||||
value=users
|
||||
onChange=(action "onChangeUsers")
|
||||
options=(hash
|
||||
topicId=topic.id
|
||||
maximum=(unless currentUser.staff 1)
|
||||
excludeCurrentUser=true
|
||||
)
|
||||
}}
|
||||
{{d-button
|
||||
icon="check"
|
||||
class="btn-primary"
|
||||
disabled=(if users false true)
|
||||
action=(action "notifyUsers")
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</form>
|
||||
{{/d-modal-body}}
|
||||
|
||||
@ -25,17 +25,19 @@
|
||||
{{i18n "user.preferences_nav.notifications"}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
<li class="indent nav-categories">
|
||||
{{#link-to "preferences.categories"}}
|
||||
{{i18n "user.preferences_nav.categories"}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
{{#if model.can_change_tracking_preferences}}
|
||||
<li class="indent nav-categories">
|
||||
{{#link-to "preferences.categories"}}
|
||||
{{i18n "user.preferences_nav.categories"}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
{{/if}}
|
||||
<li class="indent nav-users">
|
||||
{{#link-to "preferences.users"}}
|
||||
{{i18n "user.preferences_nav.users"}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
{{#if siteSettings.tagging_enabled}}
|
||||
{{#if (and model.can_change_tracking_preferences siteSettings.tagging_enabled)}}
|
||||
<li class="indent nav-tags">
|
||||
{{#link-to "preferences.tags"}}
|
||||
{{i18n "user.preferences_nav.tags"}}
|
||||
|
||||
@ -151,13 +151,21 @@
|
||||
{{#if siteSettings.enable_badges}}
|
||||
<div class="top-section badges-section">
|
||||
<h3 class="stats-title">{{i18n "user.summary.top_badges"}}</h3>
|
||||
{{#each model.badges as |badge|}}
|
||||
{{badge-card badge=badge count=badge.count username=user.username_lower}}
|
||||
|
||||
{{#if model.badges}}
|
||||
<div class="badge-group-list">
|
||||
{{#each model.badges as |badge|}}
|
||||
{{badge-card badge=badge count=badge.count username=user.username_lower}}
|
||||
{{/each}}
|
||||
</div>
|
||||
{{else}}
|
||||
<p>{{i18n "user.summary.no_badges"}}</p>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
|
||||
{{#if moreBadges}}
|
||||
<p>{{#link-to "user.badges" user class="more"}}{{i18n "user.summary.more_badges"}}{{/link-to}}</p>
|
||||
{{#link-to "user.badges" user class="more"}}
|
||||
{{i18n "user.summary.more_badges"}}
|
||||
{{/link-to}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
@ -30,7 +30,7 @@ export default createWidget("embedded-post", {
|
||||
h("div.row", [
|
||||
this.attach("post-avatar", attrs),
|
||||
h("div.topic-body", [
|
||||
h("div.topic-meta-data", [
|
||||
h("div.topic-meta-data.embedded-reply", [
|
||||
this.attach("poster-name", attrs),
|
||||
this.attach("post-link-arrow", {
|
||||
above: state.above,
|
||||
|
||||
@ -2,7 +2,6 @@ import DiscourseURL, { userPath } from "discourse/lib/url";
|
||||
import I18n from "I18n";
|
||||
import { addExtraUserClasses } from "discourse/helpers/user-avatar";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { applySearchAutocomplete } from "discourse/lib/search";
|
||||
import { avatarImg } from "discourse/widgets/post";
|
||||
import { createWidget } from "discourse/widgets/widget";
|
||||
import { get } from "@ember/object";
|
||||
@ -484,17 +483,9 @@ export default createWidget("header", {
|
||||
|
||||
if (this.state.searchVisible) {
|
||||
schedule("afterRender", () => {
|
||||
const $searchInput = $("#search-term");
|
||||
$searchInput.focus().select();
|
||||
|
||||
applySearchAutocomplete(
|
||||
$searchInput,
|
||||
this.siteSettings,
|
||||
this.appEvents,
|
||||
{
|
||||
appendSelector: ".menu-panel",
|
||||
}
|
||||
);
|
||||
const searchInput = document.querySelector("#search-term");
|
||||
searchInput.focus();
|
||||
searchInput.select();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@ -755,6 +755,9 @@ export default createWidget("post", {
|
||||
if (attrs.topicOwner) {
|
||||
classNames.push("topic-owner");
|
||||
}
|
||||
if (this.currentUser && attrs.user_id === this.currentUser.id) {
|
||||
classNames.push("current-user-post");
|
||||
}
|
||||
if (attrs.groupModerator) {
|
||||
classNames.push("category-moderator");
|
||||
}
|
||||
|
||||
@ -13,10 +13,6 @@ createWidget("search-term", {
|
||||
return { afterAutocomplete: false };
|
||||
},
|
||||
|
||||
searchAutocompleteAfterComplete() {
|
||||
this.state.afterAutocomplete = true;
|
||||
},
|
||||
|
||||
buildAttributes(attrs) {
|
||||
return {
|
||||
type: "text",
|
||||
@ -28,12 +24,8 @@ createWidget("search-term", {
|
||||
},
|
||||
|
||||
keyUp(e) {
|
||||
if (e.which === 13) {
|
||||
if (this.state.afterAutocomplete) {
|
||||
this.state.afterAutocomplete = false;
|
||||
} else {
|
||||
return this.sendWidgetAction("fullSearch");
|
||||
}
|
||||
if (e.which === 13 && !this.state.afterAutocomplete) {
|
||||
return this.sendWidgetAction("fullSearch");
|
||||
}
|
||||
|
||||
const val = this.attrs.value;
|
||||
|
||||
@ -10,6 +10,25 @@ import highlightSearch from "discourse/lib/highlight-search";
|
||||
import { iconNode } from "discourse-common/lib/icon-library";
|
||||
import renderTag from "discourse/lib/render-tag";
|
||||
|
||||
const suggestionShortcuts = [
|
||||
"in:title",
|
||||
"in:pinned",
|
||||
"status:open",
|
||||
"status:closed",
|
||||
"status:public",
|
||||
"status:noreplies",
|
||||
"order:latest",
|
||||
"order:views",
|
||||
"order:likes",
|
||||
"order:latest_topic",
|
||||
];
|
||||
|
||||
export function addSearchSuggestion(value) {
|
||||
if (suggestionShortcuts.indexOf(value) === -1) {
|
||||
suggestionShortcuts.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
class Highlighted extends RawHtml {
|
||||
constructor(html, term) {
|
||||
super({ html: `<span>${html}</span>` });
|
||||
@ -207,6 +226,14 @@ createWidget("search-menu-results", {
|
||||
tagName: "div.results",
|
||||
|
||||
html(attrs) {
|
||||
if (attrs.suggestionKeyword) {
|
||||
return this.attach("search-menu-assistant", {
|
||||
fullTerm: attrs.term,
|
||||
suggestionKeyword: attrs.suggestionKeyword,
|
||||
results: attrs.suggestionResults || [],
|
||||
});
|
||||
}
|
||||
|
||||
if (attrs.invalidTerm) {
|
||||
return h("div.no-results", I18n.t("search.too_short"));
|
||||
}
|
||||
@ -320,3 +347,135 @@ createWidget("search-menu-results", {
|
||||
return content;
|
||||
},
|
||||
});
|
||||
|
||||
createWidget("search-menu-assistant", {
|
||||
tagName: "ul.search-menu-assistant",
|
||||
|
||||
html(attrs) {
|
||||
if (this.currentUser) {
|
||||
addSearchSuggestion("in:likes");
|
||||
addSearchSuggestion("in:bookmarks");
|
||||
addSearchSuggestion("in:mine");
|
||||
addSearchSuggestion("in:personal");
|
||||
addSearchSuggestion("in:seen");
|
||||
addSearchSuggestion("in:tracking");
|
||||
addSearchSuggestion("in:unseen");
|
||||
addSearchSuggestion("in:watching");
|
||||
}
|
||||
if (this.siteSettings.tagging_enabled) {
|
||||
addSearchSuggestion("in:tagged");
|
||||
addSearchSuggestion("in:untagged");
|
||||
}
|
||||
|
||||
const content = [];
|
||||
const { fullTerm, suggestionKeyword } = attrs;
|
||||
const prefix = fullTerm.split(suggestionKeyword)[0].trim() || null;
|
||||
|
||||
switch (suggestionKeyword) {
|
||||
case "#":
|
||||
attrs.results.forEach((category) => {
|
||||
const slug = prefix
|
||||
? `${prefix} #${category.slug} `
|
||||
: `#${category.slug} `;
|
||||
|
||||
content.push(
|
||||
this.attach("search-menu-assistant-item", {
|
||||
prefix: prefix,
|
||||
category,
|
||||
slug,
|
||||
})
|
||||
);
|
||||
});
|
||||
break;
|
||||
case "@":
|
||||
attrs.results.forEach((user) => {
|
||||
const slug = prefix
|
||||
? `${prefix} @${user.username} `
|
||||
: `@${user.username} `;
|
||||
|
||||
content.push(
|
||||
this.attach("search-menu-assistant-item", {
|
||||
prefix: prefix,
|
||||
user,
|
||||
slug,
|
||||
})
|
||||
);
|
||||
});
|
||||
break;
|
||||
default:
|
||||
suggestionShortcuts.forEach((item) => {
|
||||
if (item.includes(suggestionKeyword)) {
|
||||
const slug = prefix ? `${prefix} ${item} ` : `${item} `;
|
||||
content.push(this.attach("search-menu-assistant-item", { slug }));
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
return content.filter((c, i) => i <= 8);
|
||||
},
|
||||
});
|
||||
|
||||
createWidget("search-menu-assistant-item", {
|
||||
tagName: "li.search-menu-assistant-item",
|
||||
|
||||
html(attrs) {
|
||||
if (attrs.category) {
|
||||
return h(
|
||||
"a.widget-link.search-link",
|
||||
{
|
||||
attributes: {
|
||||
href: attrs.category.url,
|
||||
},
|
||||
},
|
||||
[
|
||||
h("span.search-item-prefix", attrs.prefix),
|
||||
this.attach("category-link", {
|
||||
category: attrs.category,
|
||||
allowUncategorized: true,
|
||||
}),
|
||||
]
|
||||
);
|
||||
} else if (attrs.user) {
|
||||
const userResult = [
|
||||
avatarImg("small", {
|
||||
template: attrs.user.avatar_template,
|
||||
username: attrs.user.username,
|
||||
}),
|
||||
h("span.username", formatUsername(attrs.user.username)),
|
||||
];
|
||||
|
||||
return h(
|
||||
"a.widget-link.search-link",
|
||||
{
|
||||
attributes: {
|
||||
href: "#",
|
||||
},
|
||||
},
|
||||
[
|
||||
h("span.search-item-prefix", attrs.prefix),
|
||||
h("span.search-item-user", userResult),
|
||||
]
|
||||
);
|
||||
} else {
|
||||
return h(
|
||||
"a.widget-link.search-link",
|
||||
{
|
||||
attributes: {
|
||||
href: "#",
|
||||
},
|
||||
},
|
||||
h("span.search-item-slug", attrs.slug)
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
click(e) {
|
||||
const searchInput = document.querySelector("#search-term");
|
||||
searchInput.value = this.attrs.slug;
|
||||
searchInput.focus();
|
||||
this.sendWidgetAction("triggerAutocomplete", this.attrs.slug);
|
||||
e.preventDefault();
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { isValidSearchTerm, searchForTerm } from "discourse/lib/search";
|
||||
import Category from "discourse/models/category";
|
||||
import DiscourseURL from "discourse/lib/url";
|
||||
import { createWidget } from "discourse/widgets/widget";
|
||||
import discourseDebounce from "discourse-common/lib/debounce";
|
||||
@ -6,6 +7,11 @@ import { get } from "@ember/object";
|
||||
import getURL from "discourse-common/lib/get-url";
|
||||
import { h } from "virtual-dom";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import userSearch from "discourse/lib/user-search";
|
||||
|
||||
const CATEGORY_SLUG_REGEXP = /(\#[a-zA-Z0-9\-:]*)$/gi;
|
||||
const USERNAME_REGEXP = /(\@[a-zA-Z0-9\-\_]*)$/gi;
|
||||
const SUGGESTIONS_REGEXP = /(in:|status:|order:|:)([a-zA-Z]*)$/gi;
|
||||
|
||||
const searchData = {};
|
||||
|
||||
@ -17,6 +23,7 @@ export function initSearchData() {
|
||||
searchData.typeFilter = null;
|
||||
searchData.invalidTerm = false;
|
||||
searchData.topicId = null;
|
||||
searchData.afterAutocomplete = false;
|
||||
}
|
||||
|
||||
initSearchData();
|
||||
@ -39,6 +46,52 @@ const SearchHelper = {
|
||||
const { term, typeFilter, contextEnabled } = searchData;
|
||||
const searchContext = contextEnabled ? widget.searchContext() : null;
|
||||
const fullSearchUrl = widget.fullSearchUrl();
|
||||
const matchSuggestions = this.matchesSuggestions();
|
||||
|
||||
if (matchSuggestions) {
|
||||
searchData.noResults = true;
|
||||
searchData.results = [];
|
||||
searchData.loading = false;
|
||||
|
||||
if (matchSuggestions.type === "category") {
|
||||
const categorySearchTerm = matchSuggestions.categoriesMatch[0].replace(
|
||||
"#",
|
||||
""
|
||||
);
|
||||
|
||||
searchData.suggestionResults = Category.search(categorySearchTerm);
|
||||
searchData.suggestionKeyword = "#";
|
||||
widget.scheduleRerender();
|
||||
} else if (matchSuggestions.type === "username") {
|
||||
const userSearchTerm = matchSuggestions.usernamesMatch[0].replace(
|
||||
"@",
|
||||
""
|
||||
);
|
||||
const opts = { includeGroups: true, limit: 6 };
|
||||
if (userSearchTerm.length > 0) {
|
||||
opts.term = userSearchTerm;
|
||||
} else {
|
||||
opts.lastSeenUsers = true;
|
||||
}
|
||||
|
||||
userSearch(opts).then((result) => {
|
||||
if (result?.users?.length > 0) {
|
||||
searchData.suggestionResults = result.users;
|
||||
searchData.suggestionKeyword = "@";
|
||||
} else {
|
||||
searchData.noResults = true;
|
||||
searchData.suggestionKeyword = false;
|
||||
}
|
||||
widget.scheduleRerender();
|
||||
});
|
||||
} else {
|
||||
searchData.suggestionKeyword = matchSuggestions[0];
|
||||
widget.scheduleRerender();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
searchData.suggestionKeyword = false;
|
||||
|
||||
if (!isValidSearchTerm(term, widget.siteSettings)) {
|
||||
searchData.noResults = true;
|
||||
@ -73,10 +126,35 @@ const SearchHelper = {
|
||||
.catch(popupAjaxError)
|
||||
.finally(() => {
|
||||
searchData.loading = false;
|
||||
searchData.afterAutocomplete = false;
|
||||
widget.scheduleRerender();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
matchesSuggestions() {
|
||||
if (searchData.term === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const categoriesMatch = searchData.term.match(CATEGORY_SLUG_REGEXP);
|
||||
|
||||
if (categoriesMatch) {
|
||||
return { type: "category", categoriesMatch };
|
||||
}
|
||||
|
||||
const usernamesMatch = searchData.term.match(USERNAME_REGEXP);
|
||||
if (usernamesMatch) {
|
||||
return { type: "username", usernamesMatch };
|
||||
}
|
||||
|
||||
const suggestionsMatch = searchData.term.match(SUGGESTIONS_REGEXP);
|
||||
if (suggestionsMatch) {
|
||||
return suggestionsMatch;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
};
|
||||
|
||||
export default createWidget("search-menu", {
|
||||
@ -132,10 +210,14 @@ export default createWidget("search-menu", {
|
||||
},
|
||||
|
||||
panelContents() {
|
||||
const contextEnabled = searchData.contextEnabled;
|
||||
const { contextEnabled, afterAutocomplete } = searchData;
|
||||
|
||||
let searchInput = [
|
||||
this.attach("search-term", { value: searchData.term, contextEnabled }),
|
||||
this.attach(
|
||||
"search-term",
|
||||
{ value: searchData.term, contextEnabled },
|
||||
{ state: { afterAutocomplete } }
|
||||
),
|
||||
];
|
||||
if (searchData.term && searchData.loading) {
|
||||
searchInput.push(h("div.searching", h("div.spinner")));
|
||||
@ -157,6 +239,8 @@ export default createWidget("search-menu", {
|
||||
results: searchData.results,
|
||||
invalidTerm: searchData.invalidTerm,
|
||||
searchContextEnabled: searchData.contextEnabled,
|
||||
suggestionKeyword: searchData.suggestionKeyword,
|
||||
suggestionResults: searchData.suggestionResults,
|
||||
})
|
||||
);
|
||||
}
|
||||
@ -200,7 +284,7 @@ export default createWidget("search-menu", {
|
||||
});
|
||||
},
|
||||
|
||||
mouseDownOutside() {
|
||||
clickOutside() {
|
||||
this.sendWidgetAction("toggleSearchMenu");
|
||||
},
|
||||
|
||||
@ -211,7 +295,7 @@ export default createWidget("search-menu", {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (searchData.loading || searchData.noResults) {
|
||||
if (searchData.loading) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -306,6 +390,11 @@ export default createWidget("search-menu", {
|
||||
this.triggerSearch();
|
||||
},
|
||||
|
||||
triggerAutocomplete(term) {
|
||||
searchData.afterAutocomplete = true;
|
||||
this.searchTermChanged(term);
|
||||
},
|
||||
|
||||
fullSearch() {
|
||||
if (!isValidSearchTerm(searchData.term, this.siteSettings)) {
|
||||
return;
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import {
|
||||
acceptance,
|
||||
exists,
|
||||
query,
|
||||
queryAll,
|
||||
} from "discourse/tests/helpers/qunit-helpers";
|
||||
import { fillIn, visit } from "@ember/test-helpers";
|
||||
@ -22,7 +23,12 @@ function setAuthenticationData(hooks, json) {
|
||||
});
|
||||
}
|
||||
|
||||
function preloadInvite({ link = false, email_verified_by_link = false } = {}) {
|
||||
function preloadInvite({
|
||||
link = false,
|
||||
email_verified_by_link = false,
|
||||
different_external_email = false,
|
||||
hidden_email = false,
|
||||
} = {}) {
|
||||
const info = {
|
||||
invited_by: {
|
||||
id: 123,
|
||||
@ -33,6 +39,8 @@ function preloadInvite({ link = false, email_verified_by_link = false } = {}) {
|
||||
},
|
||||
username: "invited",
|
||||
email_verified_by_link: email_verified_by_link,
|
||||
different_external_email: different_external_email,
|
||||
hidden_email: hidden_email,
|
||||
};
|
||||
|
||||
if (link) {
|
||||
@ -362,6 +370,32 @@ acceptance(
|
||||
}
|
||||
);
|
||||
|
||||
acceptance(
|
||||
"Email Invite link with different external email address",
|
||||
function (needs) {
|
||||
needs.settings({ enable_local_logins: false });
|
||||
|
||||
setAuthenticationData(needs.hooks, {
|
||||
auth_provider: "facebook",
|
||||
email: "foobar+different@example.com",
|
||||
email_valid: true,
|
||||
username: "foobar",
|
||||
name: "barfoo",
|
||||
});
|
||||
|
||||
test("display information that email is invalid", async function (assert) {
|
||||
preloadInvite({ different_external_email: true, hidden_email: true });
|
||||
|
||||
await visit("/invites/myvalidinvitetoken");
|
||||
|
||||
assert.equal(
|
||||
query(".bad").textContent.trim(),
|
||||
"Your invitation email does not match the email authenticated by Facebook"
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
acceptance(
|
||||
"Email Invite link with valid authentication data, valid email token, unverified authentication email",
|
||||
function (needs) {
|
||||
|
||||
@ -524,3 +524,36 @@ acceptance("Security", function (needs) {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
acceptance(
|
||||
"User Preferences for staged user and don't allow tracking prefs",
|
||||
function (needs) {
|
||||
needs.settings({
|
||||
allow_changing_staged_user_tracking: false,
|
||||
tagging_enabled: true,
|
||||
});
|
||||
needs.pretender(preferencesPretender);
|
||||
|
||||
test("staged user doesn't show category and tag preferences", async function (assert) {
|
||||
await visit("/u/staged/preferences");
|
||||
|
||||
assert.ok($("body.user-preferences-page").length, "has the body class");
|
||||
assert.equal(
|
||||
currentURL(),
|
||||
"/u/staged/preferences/account",
|
||||
"defaults to account tab"
|
||||
);
|
||||
assert.ok(exists(".user-preferences"), "it shows the preferences");
|
||||
|
||||
assert.ok(
|
||||
!exists(".preferences-nav .nav-categories a"),
|
||||
"categories tab isn't there for staged users"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
!exists(".preferences-nav .nav-tags a"),
|
||||
"tags tab isn't there for staged users"
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
@ -197,7 +197,7 @@ acceptance("Review", function (needs) {
|
||||
publishToMessageBus("/reviewable_counts", {
|
||||
review_count: 1,
|
||||
updates: {
|
||||
1234: { status: 1 },
|
||||
1234: { last_performing_username: "foo", status: 1 },
|
||||
},
|
||||
});
|
||||
|
||||
@ -206,5 +206,11 @@ acceptance("Review", function (needs) {
|
||||
assert.ok(reviewable.className.includes("reviewable-stale"));
|
||||
assert.equal(count("[data-reviewable-id=1234] .status .approved"), 1);
|
||||
assert.equal(count(".stale-help"), 1);
|
||||
assert.ok(query(".stale-help").innerText.includes("foo"));
|
||||
|
||||
await visit("/");
|
||||
await visit("/review"); // reload review
|
||||
|
||||
assert.equal(count(".stale-help"), 0);
|
||||
});
|
||||
});
|
||||
|
||||
@ -2,6 +2,7 @@ import {
|
||||
acceptance,
|
||||
count,
|
||||
exists,
|
||||
query,
|
||||
queryAll,
|
||||
} from "discourse/tests/helpers/qunit-helpers";
|
||||
import { click, fillIn, triggerKeyEvent, visit } from "@ember/test-helpers";
|
||||
@ -51,6 +52,22 @@ acceptance("Search - Anonymous", function (needs) {
|
||||
);
|
||||
});
|
||||
|
||||
test("search button toggles search menu", async function (assert) {
|
||||
await visit("/");
|
||||
|
||||
await click("#search-button");
|
||||
assert.ok(exists(".search-menu"));
|
||||
|
||||
await click(".d-header"); // click outside
|
||||
assert.ok(!exists(".search-menu"));
|
||||
|
||||
await click("#search-button");
|
||||
assert.ok(exists(".search-menu"));
|
||||
|
||||
await click("#search-button"); // toggle same button
|
||||
assert.ok(!exists(".search-menu"));
|
||||
});
|
||||
|
||||
test("search for a tag", async function (assert) {
|
||||
await visit("/");
|
||||
|
||||
@ -247,3 +264,105 @@ acceptance("Search - with tagging enabled", function (needs) {
|
||||
assert.equal(tags, "dev slow");
|
||||
});
|
||||
});
|
||||
|
||||
acceptance("Search - assistant", function (needs) {
|
||||
needs.user();
|
||||
|
||||
needs.pretender((server, helper) => {
|
||||
server.get("/u/search/users", () => {
|
||||
return helper.response({
|
||||
users: [
|
||||
{
|
||||
username: "TeaMoe",
|
||||
name: "TeaMoe",
|
||||
avatar_template:
|
||||
"https://avatars.discourse.org/v3/letter/t/41988e/{size}.png",
|
||||
},
|
||||
{
|
||||
username: "TeamOneJ",
|
||||
name: "J Cobb",
|
||||
avatar_template:
|
||||
"https://avatars.discourse.org/v3/letter/t/3d9bf3/{size}.png",
|
||||
},
|
||||
{
|
||||
username: "kudos",
|
||||
name: "Team Blogeto.com",
|
||||
avatar_template:
|
||||
"/user_avatar/meta.discourse.org/kudos/{size}/62185_1.png",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("shows category shortcuts when typing #", async function (assert) {
|
||||
await visit("/");
|
||||
|
||||
await click("#search-button");
|
||||
|
||||
await fillIn("#search-term", "#");
|
||||
await triggerKeyEvent("#search-term", "keyup", 51);
|
||||
|
||||
const firstCategory =
|
||||
".search-menu .results ul.search-menu-assistant .search-link";
|
||||
assert.ok(exists(query(firstCategory)));
|
||||
|
||||
const firstResultSlug = query(
|
||||
`${firstCategory} .category-name`
|
||||
).innerText.trim();
|
||||
|
||||
await click(firstCategory);
|
||||
assert.equal(query("#search-term").value, `#${firstResultSlug} `);
|
||||
|
||||
await fillIn("#search-term", "sam #");
|
||||
await triggerKeyEvent("#search-term", "keyup", 51);
|
||||
|
||||
assert.ok(exists(query(firstCategory)));
|
||||
assert.equal(
|
||||
query(
|
||||
".search-menu .results ul.search-menu-assistant .search-item-prefix"
|
||||
).innerText,
|
||||
"sam"
|
||||
);
|
||||
|
||||
await click(firstCategory);
|
||||
assert.equal(query("#search-term").value, `sam #${firstResultSlug} `);
|
||||
});
|
||||
|
||||
test("shows in: shortcuts", async function (assert) {
|
||||
await visit("/");
|
||||
await click("#search-button");
|
||||
|
||||
const firstTarget =
|
||||
".search-menu .results ul.search-menu-assistant .search-link .search-item-slug";
|
||||
|
||||
await fillIn("#search-term", "in:");
|
||||
await triggerKeyEvent("#search-term", "keyup", 51);
|
||||
assert.equal(query(firstTarget).innerText, "in:title");
|
||||
|
||||
await fillIn("#search-term", "sam in:");
|
||||
await triggerKeyEvent("#search-term", "keyup", 51);
|
||||
assert.equal(query(firstTarget).innerText, "sam in:title");
|
||||
|
||||
await fillIn("#search-term", "in:pers");
|
||||
await triggerKeyEvent("#search-term", "keyup", 51);
|
||||
assert.equal(query(firstTarget).innerText, "in:personal");
|
||||
});
|
||||
|
||||
test("shows users when typing @", async function (assert) {
|
||||
await visit("/");
|
||||
|
||||
await click("#search-button");
|
||||
|
||||
await fillIn("#search-term", "@");
|
||||
await triggerKeyEvent("#search-term", "keyup", 51);
|
||||
|
||||
const firstUser =
|
||||
".search-menu .results ul.search-menu-assistant .search-item-user";
|
||||
const firstUsername = query(firstUser).innerText.trim();
|
||||
assert.equal(firstUsername, "TeaMoe");
|
||||
|
||||
await click(query(firstUser));
|
||||
assert.equal(query("#search-term").value, `@${firstUsername} `);
|
||||
});
|
||||
});
|
||||
|
||||
@ -30,7 +30,7 @@ acceptance("Share and Invite modal", function (needs) {
|
||||
"it shows the topic sharing url"
|
||||
);
|
||||
|
||||
assert.ok(count("button[class^='share-']") > 1, "it shows social sources");
|
||||
assert.ok(count("button[class*='share-']") > 1, "it shows social sources");
|
||||
|
||||
assert.ok(
|
||||
exists(".btn[aria-label='Notify']"),
|
||||
|
||||
@ -40,7 +40,7 @@ acceptance("Topic", function (needs) {
|
||||
test("Reply as new topic", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
await click("button.share:nth-of-type(1)");
|
||||
await click(".reply-as-new-topic a");
|
||||
await click("button.new-topic");
|
||||
|
||||
assert.ok(exists(".d-editor-input"), "the composer input is visible");
|
||||
|
||||
@ -59,7 +59,7 @@ acceptance("Topic", function (needs) {
|
||||
test("Reply as new message", async function (assert) {
|
||||
await visit("/t/pm-for-testing/12");
|
||||
await click("button.share:nth-of-type(1)");
|
||||
await click(".reply-as-new-topic a");
|
||||
await click("button.new-topic");
|
||||
|
||||
assert.ok(exists(".d-editor-input"), "the composer input is visible");
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -377,7 +377,7 @@ export default {
|
||||
uploaded_avatar_id: 40181,
|
||||
created_at: "2013-02-05T21:32:47.649Z",
|
||||
cooked:
|
||||
'<p>The application strings <a href="https://github.com/discourse/discourse/blob/master/config/locales/en.yml" rel="nofollow">are externalized</a>, so localization should be entirely possible with enough translation effort.</p><p>Link for unknown-test: <a href="/c/category-does-not-exist/99999" data-for-test="category-404">link</a></p>',
|
||||
'<p>The application strings <a href="https://github.com/discourse/discourse/blob/main/config/locales/en.yml" rel="nofollow">are externalized</a>, so localization should be entirely possible with enough translation effort.</p><p>Link for unknown-test: <a href="/c/category-does-not-exist/99999" data-for-test="category-404">link</a></p>',
|
||||
post_number: 2,
|
||||
post_type: 1,
|
||||
updated_at: "2013-02-06T10:15:27.965Z",
|
||||
@ -400,7 +400,7 @@ export default {
|
||||
link_counts: [
|
||||
{
|
||||
url:
|
||||
"https://github.com/discourse/discourse/blob/master/config/locales/en.yml",
|
||||
"https://github.com/discourse/discourse/blob/main/config/locales/en.yml",
|
||||
internal: false,
|
||||
reflection: false,
|
||||
clicks: 118,
|
||||
@ -473,7 +473,7 @@ export default {
|
||||
uploaded_avatar_id: 5297,
|
||||
created_at: "2013-02-06T02:26:24.922Z",
|
||||
cooked:
|
||||
'<p>Yep, all strings are going through a lookup table.*</p>\n\n<p><a href="https://github.com/discourse/discourse/blob/master/config/locales">master/config/locales</a></p>\n\n<p>So you could replace that lookup table with the "de" one to get German.</p>\n\n<p><sub>* we didn\'t get all the strings into the lookup table for launch, but the vast, vast majority of them are and the ones that are not, we will fix as we go!</sub></p>',
|
||||
'<p>Yep, all strings are going through a lookup table.*</p>\n\n<p><a href="https://github.com/discourse/discourse/blob/main/config/locales">master/config/locales</a></p>\n\n<p>So you could replace that lookup table with the "de" one to get German.</p>\n\n<p><sub>* we didn\'t get all the strings into the lookup table for launch, but the vast, vast majority of them are and the ones that are not, we will fix as we go!</sub></p>',
|
||||
post_number: 3,
|
||||
post_type: 1,
|
||||
updated_at: "2014-02-24T05:23:39.211Z",
|
||||
@ -496,7 +496,7 @@ export default {
|
||||
link_counts: [
|
||||
{
|
||||
url:
|
||||
"https://github.com/discourse/discourse/blob/master/config/locales",
|
||||
"https://github.com/discourse/discourse/blob/main/config/locales",
|
||||
internal: false,
|
||||
reflection: false,
|
||||
title:
|
||||
@ -2405,7 +2405,7 @@ export default {
|
||||
links: [
|
||||
{
|
||||
url:
|
||||
"https://github.com/discourse/discourse/blob/master/config/locales/en.yml",
|
||||
"https://github.com/discourse/discourse/blob/main/config/locales/en.yml",
|
||||
title: null,
|
||||
fancy_title: null,
|
||||
internal: false,
|
||||
@ -2438,7 +2438,7 @@ export default {
|
||||
},
|
||||
{
|
||||
url:
|
||||
"https://github.com/discourse/discourse/blob/master/config/locales",
|
||||
"https://github.com/discourse/discourse/blob/main/config/locales",
|
||||
title:
|
||||
"discourse/config/locales at master · discourse/discourse · GitHub",
|
||||
fancy_title: null,
|
||||
@ -2521,7 +2521,7 @@ export default {
|
||||
},
|
||||
{
|
||||
url:
|
||||
"https://github.com/discourse/discourse/tree/master/config/locales",
|
||||
"https://github.com/discourse/discourse/tree/main/config/locales",
|
||||
title:
|
||||
"discourse/config/locales at master · discourse/discourse · GitHub",
|
||||
fancy_title: null,
|
||||
@ -2566,7 +2566,7 @@ export default {
|
||||
},
|
||||
{
|
||||
url:
|
||||
"https://github.com/discourse/discourse/blob/master/config/locales/client.en.yml#L691",
|
||||
"https://github.com/discourse/discourse/blob/main/config/locales/client.en.yml#L691",
|
||||
title:
|
||||
"discourse/config/locales/client.en.yml at master · discourse/discourse · GitHub",
|
||||
fancy_title: null,
|
||||
@ -2588,7 +2588,7 @@ export default {
|
||||
},
|
||||
{
|
||||
url:
|
||||
"https://github.com/discourse/discourse/blob/master/config/locales/client.nl.yml",
|
||||
"https://github.com/discourse/discourse/blob/main/config/locales/client.nl.yml",
|
||||
title:
|
||||
"discourse/config/locales/client.nl.yml at master · discourse/discourse · GitHub",
|
||||
fancy_title: null,
|
||||
@ -2820,7 +2820,7 @@ export default {
|
||||
},
|
||||
{
|
||||
url:
|
||||
"https://github.com/discourse/discourse/blob/master/app/assets/javascripts/locales/date_locales.js",
|
||||
"https://github.com/discourse/discourse/blob/main/app/assets/javascripts/locales/date_locales.js",
|
||||
title: null,
|
||||
fancy_title: null,
|
||||
internal: false,
|
||||
@ -2885,7 +2885,7 @@ export default {
|
||||
},
|
||||
{
|
||||
url:
|
||||
"https://github.com/discourse/discourse/blob/master/config/locales/client.de.yml",
|
||||
"https://github.com/discourse/discourse/blob/main/config/locales/client.de.yml",
|
||||
title:
|
||||
"discourse/config/locales/client.de.yml at master · discourse/discourse · GitHub",
|
||||
fancy_title: null,
|
||||
@ -2919,7 +2919,7 @@ export default {
|
||||
},
|
||||
{
|
||||
url:
|
||||
"https://github.com/discourse/discourse/blob/master/config/locales/server.de.yml",
|
||||
"https://github.com/discourse/discourse/blob/main/config/locales/server.de.yml",
|
||||
title:
|
||||
"discourse/config/locales/server.de.yml at master · discourse/discourse · GitHub",
|
||||
fancy_title: null,
|
||||
|
||||
@ -190,6 +190,7 @@ export default {
|
||||
card_image_badge_id: 120,
|
||||
muted_usernames: [],
|
||||
can_change_location: true,
|
||||
can_change_tracking_preferences: true,
|
||||
ignored_usernames: [],
|
||||
invited_by: {
|
||||
id: 1,
|
||||
@ -2534,6 +2535,7 @@ export default {
|
||||
ignored_usernames: [],
|
||||
mailing_list_posts_per_day: 0,
|
||||
can_change_bio: true,
|
||||
can_change_tracking_preferences: true,
|
||||
user_api_keys: null,
|
||||
user_auth_tokens: [],
|
||||
invited_by: null,
|
||||
@ -3322,4 +3324,127 @@ export default {
|
||||
timezone: "Australia/Brisbane",
|
||||
},
|
||||
},
|
||||
"/u/staged.json": {
|
||||
user_badges: [],
|
||||
badges: [],
|
||||
badge_types: [
|
||||
{ id: 1, name: "Gold", sort_order: 9 },
|
||||
{ id: 2, name: "Silver", sort_order: 8 },
|
||||
{ id: 3, name: "Bronze", sort_order: 7 },
|
||||
],
|
||||
users: [
|
||||
{
|
||||
id: 20,
|
||||
username: "staged",
|
||||
uploaded_avatar_id: null,
|
||||
avatar_template:
|
||||
"/letter_avatar/staged/{size}/3_f9720745f5ce6dfc2b5641fca999d934.png",
|
||||
},
|
||||
{
|
||||
id: -1,
|
||||
username: "system",
|
||||
uploaded_avatar_id: null,
|
||||
avatar_template:
|
||||
"/letter_avatar/system/{size}/3_f9720745f5ce6dfc2b5641fca999d934.png",
|
||||
},
|
||||
],
|
||||
topics: [],
|
||||
user: {
|
||||
user_option: {
|
||||
text_size_seq: 1,
|
||||
},
|
||||
id: 20,
|
||||
username: "staged",
|
||||
staged: true,
|
||||
uploaded_avatar_id: null,
|
||||
avatar_template:
|
||||
"/letter_avatar/staged/{size}/3_f9720745f5ce6dfc2b5641fca999d934.png",
|
||||
name: "Staged",
|
||||
email: "staged.user@example.com",
|
||||
associated_accounts: [],
|
||||
last_posted_at: "2015-05-07T15:23:35.074Z",
|
||||
last_seen_at: "2015-05-13T14:34:23.188Z",
|
||||
bio_raw: "",
|
||||
bio_cooked: "",
|
||||
created_at: "2013-02-03T15:19:22.704Z",
|
||||
website: "",
|
||||
location: "",
|
||||
can_edit: true,
|
||||
can_edit_username: true,
|
||||
can_edit_email: true,
|
||||
can_edit_name: true,
|
||||
stats: [],
|
||||
can_send_private_messages: true,
|
||||
can_send_private_message_to_user: true,
|
||||
bio_excerpt: "",
|
||||
trust_level: 0,
|
||||
moderator: false,
|
||||
admin: false,
|
||||
title: null,
|
||||
badge_count: 0,
|
||||
notification_count: 0,
|
||||
has_title_badges: false,
|
||||
custom_fields: {},
|
||||
user_fields: {},
|
||||
pending_count: 0,
|
||||
post_count: 0,
|
||||
can_be_deleted: true,
|
||||
can_delete_all_posts: true,
|
||||
locale: "",
|
||||
email_digests: false,
|
||||
email_messages_level: 0,
|
||||
email_level: 1,
|
||||
digest_after_minutes: 10080,
|
||||
mailing_list_mode: false,
|
||||
auto_track_topics_after_msecs: 60000,
|
||||
new_topic_duration_minutes: 1440,
|
||||
external_links_in_new_tab: false,
|
||||
dynamic_favicon: true,
|
||||
skip_new_user_tips: false,
|
||||
enable_quoting: true,
|
||||
muted_category_ids: [],
|
||||
regular_category_ids: [],
|
||||
tracked_category_ids: [],
|
||||
watched_category_ids: [],
|
||||
watched_first_post_category_ids: [],
|
||||
private_messages_stats: {},
|
||||
gravatar_avatar_upload_id: 5275,
|
||||
custom_avatar_upload_id: 1573,
|
||||
card_image_badge: "/images/avatar.png",
|
||||
card_image_badge_id: 120,
|
||||
muted_usernames: [],
|
||||
can_change_location: true,
|
||||
can_change_tracking_preferences: false,
|
||||
ignored_usernames: [],
|
||||
invited_by: {
|
||||
id: 1,
|
||||
username: "sam",
|
||||
uploaded_avatar_id: null,
|
||||
avatar_template:
|
||||
"/letter_avatar/sam/{size}/3_f9720745f5ce6dfc2b5641fca999d934.png",
|
||||
},
|
||||
custom_groups: [],
|
||||
featured_user_badge_ids: [],
|
||||
card_badge: null,
|
||||
user_auth_tokens: [],
|
||||
user_notification_schedule: {
|
||||
enabled: false,
|
||||
day_0_start_time: 480,
|
||||
day_0_end_time: 1020,
|
||||
day_1_start_time: 480,
|
||||
day_1_end_time: 1020,
|
||||
day_2_start_time: 480,
|
||||
day_2_end_time: 1020,
|
||||
day_3_start_time: 480,
|
||||
day_3_end_time: 1020,
|
||||
day_4_start_time: 480,
|
||||
day_4_end_time: 1020,
|
||||
day_5_start_time: 480,
|
||||
day_5_end_time: 1020,
|
||||
day_6_start_time: 480,
|
||||
day_6_end_time: 1020,
|
||||
},
|
||||
timezone: "Australia/Brisbane",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@ -0,0 +1,78 @@
|
||||
import componentTest, {
|
||||
setupRenderingTest,
|
||||
} from "discourse/tests/helpers/component-test";
|
||||
import { discourseModule } from "discourse/tests/helpers/qunit-helpers";
|
||||
import hbs from "htmlbars-inline-precompile";
|
||||
import { triggerEvent } from "@ember/test-helpers";
|
||||
import sinon from "sinon";
|
||||
|
||||
function createBlob(mimeType, extension) {
|
||||
const blob = new Blob(["content"], {
|
||||
type: mimeType,
|
||||
});
|
||||
blob.name = `filename${extension}`;
|
||||
return blob;
|
||||
}
|
||||
|
||||
discourseModule(
|
||||
"Integration | Component | pick-files-button",
|
||||
function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
componentTest(
|
||||
"it shows alert if a file with an unsupported extension was chosen",
|
||||
{
|
||||
skip: true,
|
||||
template: hbs`
|
||||
{{pick-files-button
|
||||
acceptedFileTypes=this.acceptedFileTypes
|
||||
onFilesChosen=this.onFilesChosen}}`,
|
||||
|
||||
beforeEach() {
|
||||
const expectedExtension = ".json";
|
||||
this.set("acceptedFileTypes", [expectedExtension]);
|
||||
this.set("onFilesChosen", () => {});
|
||||
},
|
||||
|
||||
async test(assert) {
|
||||
sinon.stub(bootbox, "alert");
|
||||
|
||||
const wrongExtension = ".txt";
|
||||
const file = createBlob("text/json", wrongExtension);
|
||||
|
||||
await triggerEvent("input#file-input", "change", { files: [file] });
|
||||
|
||||
assert.ok(bootbox.alert.calledOnce);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
componentTest(
|
||||
"it shows alert if a file with an unsupported MIME type was chosen",
|
||||
{
|
||||
skip: true,
|
||||
template: hbs`
|
||||
{{pick-files-button
|
||||
acceptedFileTypes=this.acceptedFileTypes
|
||||
onFilesChosen=this.onFilesChosen}}`,
|
||||
|
||||
beforeEach() {
|
||||
const expectedMimeType = "text/json";
|
||||
this.set("acceptedFileTypes", [expectedMimeType]);
|
||||
this.set("onFilesChosen", () => {});
|
||||
},
|
||||
|
||||
async test(assert) {
|
||||
sinon.stub(bootbox, "alert");
|
||||
|
||||
const wrongMimeType = "text/plain";
|
||||
const file = createBlob(wrongMimeType, ".json");
|
||||
|
||||
await triggerEvent("input#file-input", "change", { files: [file] });
|
||||
|
||||
assert.ok(bootbox.alert.calledOnce);
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
@ -6,26 +6,37 @@ import componentTest, {
|
||||
import {
|
||||
count,
|
||||
discourseModule,
|
||||
exists,
|
||||
query,
|
||||
queryAll,
|
||||
} from "discourse/tests/helpers/qunit-helpers";
|
||||
import hbs from "htmlbars-inline-precompile";
|
||||
import { click, fillIn } from "@ember/test-helpers";
|
||||
|
||||
function createThemes(itemsCount, customAttributesCallback) {
|
||||
return [...Array(itemsCount)].map((_, i) => {
|
||||
const attrs = { name: `Theme ${i + 1}` };
|
||||
if (customAttributesCallback) {
|
||||
Object.assign(attrs, customAttributesCallback(i + 1));
|
||||
}
|
||||
return Theme.create(attrs);
|
||||
});
|
||||
}
|
||||
|
||||
discourseModule("Integration | Component | themes-list", function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
componentTest("current tab is themes", {
|
||||
template: hbs`{{themes-list themes=themes components=components currentTab=currentTab}}`,
|
||||
beforeEach() {
|
||||
this.themes = [1, 2, 3, 4, 5].map((num) =>
|
||||
Theme.create({ name: `Theme ${num}` })
|
||||
);
|
||||
this.components = [1, 2, 3, 4, 5].map((num) =>
|
||||
Theme.create({
|
||||
name: `Child ${num}`,
|
||||
this.themes = createThemes(5);
|
||||
this.components = createThemes(5, (n) => {
|
||||
return {
|
||||
name: `Child ${n}`,
|
||||
component: true,
|
||||
parentThemes: [this.themes[num - 1]],
|
||||
parentThemes: [this.themes[n - 1]],
|
||||
parent_themes: [1, 2, 3, 4, 5],
|
||||
})
|
||||
);
|
||||
};
|
||||
});
|
||||
this.setProperties({
|
||||
themes: this.themes,
|
||||
components: this.components,
|
||||
@ -94,17 +105,15 @@ discourseModule("Integration | Component | themes-list", function (hooks) {
|
||||
componentTest("current tab is components", {
|
||||
template: hbs`{{themes-list themes=themes components=components currentTab=currentTab}}`,
|
||||
beforeEach() {
|
||||
this.themes = [1, 2, 3, 4, 5].map((num) =>
|
||||
Theme.create({ name: `Theme ${num}` })
|
||||
);
|
||||
this.components = [1, 2, 3, 4, 5].map((num) =>
|
||||
Theme.create({
|
||||
name: `Child ${num}`,
|
||||
this.themes = createThemes(5);
|
||||
this.components = createThemes(5, (n) => {
|
||||
return {
|
||||
name: `Child ${n}`,
|
||||
component: true,
|
||||
parentThemes: [this.themes[num - 1]],
|
||||
parentThemes: [this.themes[n - 1]],
|
||||
parent_themes: [1, 2, 3, 4, 5],
|
||||
})
|
||||
);
|
||||
};
|
||||
});
|
||||
this.setProperties({
|
||||
themes: this.themes,
|
||||
components: this.components,
|
||||
@ -144,4 +153,139 @@ discourseModule("Integration | Component | themes-list", function (hooks) {
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
componentTest(
|
||||
"themes filter is not visible when there are less than 10 themes",
|
||||
{
|
||||
template: hbs`{{themes-list themes=themes components=[] currentTab=currentTab}}`,
|
||||
|
||||
beforeEach() {
|
||||
const themes = createThemes(9);
|
||||
this.setProperties({
|
||||
themes,
|
||||
currentTab: THEMES,
|
||||
});
|
||||
},
|
||||
async test(assert) {
|
||||
assert.ok(
|
||||
!exists(".themes-list-filter"),
|
||||
"filter input not shown when we have fewer than 10 themes"
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
componentTest(
|
||||
"themes filter keeps themes whose names include the filter term",
|
||||
{
|
||||
template: hbs`{{themes-list themes=themes components=[] currentTab=currentTab}}`,
|
||||
|
||||
beforeEach() {
|
||||
const themes = ["osama", "OsAmaa", "osAMA 1234"]
|
||||
.map((name) => Theme.create({ name: `Theme ${name}` }))
|
||||
.concat(createThemes(7));
|
||||
this.setProperties({
|
||||
themes,
|
||||
currentTab: THEMES,
|
||||
});
|
||||
},
|
||||
async test(assert) {
|
||||
assert.ok(exists(".themes-list-filter"));
|
||||
await fillIn(".themes-list-filter .filter-input", " oSAma ");
|
||||
assert.deepEqual(
|
||||
Array.from(queryAll(".themes-list-item .name")).map((node) =>
|
||||
node.textContent.trim()
|
||||
),
|
||||
["Theme osama", "Theme OsAmaa", "Theme osAMA 1234"],
|
||||
"only themes whose names include the filter term are shown"
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
componentTest(
|
||||
"switching between themes and components tabs keeps the filter visible only if both tabs have at least 10 items",
|
||||
{
|
||||
template: hbs`{{themes-list themes=themes components=components currentTab=currentTab}}`,
|
||||
|
||||
beforeEach() {
|
||||
const themes = createThemes(10, (n) => {
|
||||
return { name: `Theme ${n}${n}` };
|
||||
});
|
||||
const components = createThemes(5, (n) => {
|
||||
return {
|
||||
name: `Component ${n}${n}`,
|
||||
component: true,
|
||||
parent_themes: [1],
|
||||
parentThemes: [1],
|
||||
};
|
||||
});
|
||||
this.setProperties({
|
||||
themes,
|
||||
components,
|
||||
currentTab: THEMES,
|
||||
});
|
||||
},
|
||||
async test(assert) {
|
||||
await fillIn(".themes-list-filter .filter-input", "11");
|
||||
assert.equal(
|
||||
query(".themes-list-container").textContent.trim(),
|
||||
"Theme 11",
|
||||
"only 1 theme is shown"
|
||||
);
|
||||
await click(".themes-list-header .components-tab");
|
||||
assert.ok(
|
||||
!exists(".themes-list-filter"),
|
||||
"filter input/term do not persist when we switch to the other" +
|
||||
" tab because it has fewer than 10 items"
|
||||
);
|
||||
assert.deepEqual(
|
||||
Array.from(queryAll(".themes-list-item .name")).map((node) =>
|
||||
node.textContent.trim()
|
||||
),
|
||||
[
|
||||
"Component 11",
|
||||
"Component 22",
|
||||
"Component 33",
|
||||
"Component 44",
|
||||
"Component 55",
|
||||
],
|
||||
"all components are shown"
|
||||
);
|
||||
|
||||
this.set(
|
||||
"components",
|
||||
this.components.concat(
|
||||
createThemes(5, (n) => {
|
||||
n += 5;
|
||||
return {
|
||||
name: `Component ${n}${n}`,
|
||||
component: true,
|
||||
parent_themes: [1],
|
||||
parentThemes: [1],
|
||||
};
|
||||
})
|
||||
)
|
||||
);
|
||||
assert.ok(
|
||||
exists(".themes-list-filter"),
|
||||
"filter is now shown for the components tab"
|
||||
);
|
||||
|
||||
await fillIn(".themes-list-filter .filter-input", "66");
|
||||
assert.equal(
|
||||
query(".themes-list-container").textContent.trim(),
|
||||
"Component 66",
|
||||
"only 1 component is shown"
|
||||
);
|
||||
|
||||
await click(".themes-list-header .themes-tab");
|
||||
assert.equal(
|
||||
query(".themes-list-container").textContent.trim(),
|
||||
"Theme 66",
|
||||
"filter term persisted between tabs because both have more than 10 items"
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// DO NOT EDIT THIS FILE!!!
|
||||
// Update it by running `rake javascript:update_constants`
|
||||
|
||||
export const IMAGE_VERSION = "9";
|
||||
export const IMAGE_VERSION = "10";
|
||||
|
||||
@ -4,10 +4,10 @@ import {
|
||||
enableMissingIconWarning,
|
||||
} from "discourse-common/lib/icon-library";
|
||||
import MultiSelectComponent from "select-kit/components/multi-select";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { computed } from "@ember/object";
|
||||
import { isDevelopment } from "discourse-common/config/environment";
|
||||
import { makeArray } from "discourse-common/lib/helpers";
|
||||
import { ajax } from "select-kit/lib/ajax-helper";
|
||||
|
||||
export default MultiSelectComponent.extend({
|
||||
pluginApiIdentifiers: ["icon-picker"],
|
||||
|
||||
@ -69,6 +69,10 @@ export default Component.extend(UtilsMixin, {
|
||||
);
|
||||
}),
|
||||
|
||||
dasherizedTitle: computed("title", function () {
|
||||
return (this.title || "").replace(".", "-").dasherize();
|
||||
}),
|
||||
|
||||
label: computed("rowLabel", "item.label", "title", "rowName", function () {
|
||||
const label =
|
||||
this.rowLabel ||
|
||||
|
||||
@ -0,0 +1,8 @@
|
||||
let ajax;
|
||||
if (window.Discourse) {
|
||||
ajax = requirejs("discourse/lib/ajax").ajax;
|
||||
} else {
|
||||
ajax = requirejs("wizard/lib/ajax").ajax;
|
||||
}
|
||||
|
||||
export { ajax };
|
||||
@ -1,6 +1,6 @@
|
||||
import I18n from "I18n";
|
||||
import Mixin from "@ember/object/mixin";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { ajax } from "select-kit/lib/ajax-helper";
|
||||
import getURL from "discourse-common/lib/get-url";
|
||||
import { isEmpty } from "@ember/utils";
|
||||
import { makeArray } from "discourse-common/lib/helpers";
|
||||
|
||||
@ -10,7 +10,9 @@
|
||||
{{#select-kit/select-kit-body selectKit=selectKit id=(concat selectKit.uniqueID "-body")}}
|
||||
{{#if selectKit.isLoading}}
|
||||
<span class="is-loading">
|
||||
{{loading-spinner size="small"}}
|
||||
{{#if site}}
|
||||
{{loading-spinner size="small"}}
|
||||
{{/if}}
|
||||
</span>
|
||||
{{else}}
|
||||
{{#if selectKit.filter}}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
{{#unless isHidden}}
|
||||
{{!-- filter-input-search prevents 1password from attempting autocomplete --}}
|
||||
{{input
|
||||
tabindex=-1
|
||||
class="filter-input"
|
||||
@ -6,6 +7,7 @@
|
||||
autocomplete="discourse"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
name="filter-input-search"
|
||||
autofocus=selectKit.options.autofocus
|
||||
spellcheck=false
|
||||
value=(readonly selectKit.filter)
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{{#each icons as |icon|}}
|
||||
{{d-icon icon translatedtitle=(dasherize title)}}
|
||||
{{d-icon icon translatedtitle=dasherizedTitle}}
|
||||
{{/each}}
|
||||
|
||||
<span class="name">
|
||||
|
||||
@ -14,11 +14,20 @@ export function getToken() {
|
||||
}
|
||||
|
||||
export function ajax(args) {
|
||||
let url;
|
||||
|
||||
if (arguments.length === 2) {
|
||||
url = arguments[0];
|
||||
args = arguments[1];
|
||||
} else {
|
||||
url = args.url;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
args.headers = { "X-CSRF-Token": getToken() };
|
||||
args.success = (data) => run(null, resolve, data);
|
||||
args.error = (xhr) => run(null, reject, xhr);
|
||||
args.url = getUrl(args.url);
|
||||
args.url = getUrl(url);
|
||||
jQuery.ajax(args);
|
||||
});
|
||||
}
|
||||
|
||||
@ -41,6 +41,7 @@
|
||||
--secondary-high: #{$secondary-high};
|
||||
--secondary-very-high: #{$secondary-very-high};
|
||||
|
||||
--tertiary-very-low: #{$tertiary-very-low};
|
||||
--tertiary-low: #{$tertiary-low};
|
||||
--tertiary-medium: #{$tertiary-medium};
|
||||
--tertiary-high: #{$tertiary-high};
|
||||
|
||||
@ -295,6 +295,37 @@
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.themes-list-filter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: var(--secondary);
|
||||
z-index: z("base");
|
||||
height: 3em;
|
||||
|
||||
.d-icon {
|
||||
position: absolute;
|
||||
padding-left: 0.5em;
|
||||
}
|
||||
|
||||
.filter-input {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
padding-left: 2em;
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
|
||||
~ .d-icon {
|
||||
color: var(--tertiary-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.theme.settings {
|
||||
|
||||
@ -33,6 +33,7 @@
|
||||
@import "modal";
|
||||
@import "not-found";
|
||||
@import "onebox";
|
||||
@import "personal-message";
|
||||
@import "popup-menu";
|
||||
@import "redirection";
|
||||
@import "request_access";
|
||||
|
||||
@ -322,7 +322,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: auto;
|
||||
button {
|
||||
.btn-primary {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.cancel {
|
||||
|
||||
@ -158,7 +158,8 @@ div.edit-category {
|
||||
padding: 0 1.5em 2em 0;
|
||||
}
|
||||
|
||||
.category-topic-auto-close-hours {
|
||||
.category-topic-auto-close-hours,
|
||||
.category-default-slow-mode-seconds {
|
||||
width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,8 +14,7 @@
|
||||
|
||||
.groups-header-filters-name,
|
||||
.groups-header-filters-type {
|
||||
margin-right: 5px;
|
||||
margin-bottom: 0.5em;
|
||||
margin: 0 0.5em 0.5em 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
@ -28,65 +27,68 @@
|
||||
}
|
||||
|
||||
.groups-boxes {
|
||||
display: grid;
|
||||
grid-gap: 1em;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
margin: 1em 0;
|
||||
width: 100%;
|
||||
@supports (display: grid) {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 24%);
|
||||
grid-column-gap: 1.333%;
|
||||
grid-row-gap: 1em;
|
||||
@include breakpoint("tablet") {
|
||||
grid-template-columns: repeat(3, 32%);
|
||||
grid-column-gap: 2%;
|
||||
}
|
||||
@include breakpoint("mobile-large") {
|
||||
grid-template-columns: 100%;
|
||||
}
|
||||
|
||||
@include breakpoint("medium") {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
@include breakpoint("mobile-extra-large") {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.group-box {
|
||||
@include breakpoint("mobile-large") {
|
||||
margin: 0;
|
||||
}
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--primary-low);
|
||||
color: var(--primary);
|
||||
|
||||
.discourse-no-touch & {
|
||||
transition: all 0.25s;
|
||||
|
||||
&:hover {
|
||||
box-shadow: shadow("card");
|
||||
}
|
||||
}
|
||||
|
||||
.group-membership {
|
||||
color: var(--primary-medium);
|
||||
margin-top: auto;
|
||||
padding-top: 1em;
|
||||
|
||||
.is-group-owner,
|
||||
.is-group-member {
|
||||
color: var(--success);
|
||||
}
|
||||
}
|
||||
|
||||
.group-box-inner {
|
||||
padding: 1em;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
min-height: 8em;
|
||||
|
||||
.group-info-wrapper {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
flex: 0 1 auto;
|
||||
margin-bottom: 0.25em;
|
||||
min-height: 40px;
|
||||
overflow: hidden;
|
||||
|
||||
.group-avatar-flair {
|
||||
margin-top: 0.2em;
|
||||
margin-right: 8px;
|
||||
flex: 0 0 auto;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.group-info {
|
||||
flex: 1 0 auto;
|
||||
margin-bottom: 1em;
|
||||
width: 70%;
|
||||
flex: 1 1 auto;
|
||||
|
||||
span {
|
||||
width: 100%;
|
||||
@ -95,6 +97,7 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.group-user-count {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@ -631,7 +631,9 @@
|
||||
&.single-tab {
|
||||
background: none;
|
||||
color: var(--primary);
|
||||
padding: 4px 0;
|
||||
padding: 0;
|
||||
font-size: var(--font-up-3);
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -838,166 +840,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.create-invite-modal,
|
||||
.share-topic-modal {
|
||||
.input-group {
|
||||
margin-bottom: 0.5em;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
textarea#invite-message,
|
||||
&.invite-to-topic input[type="text"],
|
||||
.group-chooser,
|
||||
.user-chooser,
|
||||
.future-date-input-selector {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&.invite-to-topic input[type="radio"] {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
label .d-icon {
|
||||
color: var(--primary-medium);
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.input-group input[type="text"],
|
||||
.input-group .btn,
|
||||
.user-chooser .select-kit-header,
|
||||
.future-date-input .select-kit-header {
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
.invite-input-with-button {
|
||||
display: flex;
|
||||
|
||||
.btn {
|
||||
margin-left: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.input-group.input-expires-at,
|
||||
.input-group.input-email,
|
||||
.input-group.invite-max-redemptions {
|
||||
margin-bottom: 0;
|
||||
|
||||
input[type="text"] {
|
||||
width: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.future-date-input {
|
||||
.date-picker-wrapper {
|
||||
input {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.control-group:nth-child(2),
|
||||
.control-group:nth-child(3) {
|
||||
display: inline-block;
|
||||
margin-bottom: 0;
|
||||
width: 49%;
|
||||
|
||||
input {
|
||||
margin-bottom: 0;
|
||||
width: 150px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input-group.input-email {
|
||||
align-items: baseline;
|
||||
display: flex;
|
||||
|
||||
label {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.invite-input-with-button {
|
||||
display: inline-flex;
|
||||
flex: 1;
|
||||
|
||||
input[type="text"] {
|
||||
flex: 1;
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.invite-max-redemptions {
|
||||
label {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
.invite-to-topic {
|
||||
#choose-topic-title {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.show-advanced {
|
||||
margin-left: auto;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.create-invite-modal {
|
||||
.input-group {
|
||||
textarea#invite-message,
|
||||
&.invite-to-topic input[type="text"],
|
||||
.group-chooser,
|
||||
.user-chooser,
|
||||
.future-date-input-selector {
|
||||
margin-left: 25px;
|
||||
width: calc(100% - 25px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.share-topic-modal {
|
||||
form {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.sources {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: stretch;
|
||||
button {
|
||||
margin-right: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.share-twitter {
|
||||
.d-icon {
|
||||
color: var(--twitter);
|
||||
}
|
||||
}
|
||||
.share-facebook {
|
||||
.d-icon {
|
||||
color: var(--facebook);
|
||||
}
|
||||
}
|
||||
|
||||
.invite-users {
|
||||
margin: 1em 0;
|
||||
}
|
||||
}
|
||||
|
||||
.group-add-members-modal {
|
||||
.input-group {
|
||||
margin-bottom: 0.5em;
|
||||
|
||||
156
app/assets/stylesheets/common/base/personal-message.scss
Normal file
156
app/assets/stylesheets/common/base/personal-message.scss
Normal file
@ -0,0 +1,156 @@
|
||||
.archetype-private_message {
|
||||
--border-radius: 2em;
|
||||
|
||||
@keyframes current-user-background-fade-highlight {
|
||||
0% {
|
||||
background-color: var(--secondary);
|
||||
border-color: var(--primary-low);
|
||||
}
|
||||
100% {
|
||||
background-color: var(--tertiary-very-low);
|
||||
border-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.topic-body .cooked {
|
||||
box-sizing: border-box;
|
||||
border: 1px solid var(--primary-low);
|
||||
margin-top: 0.25em;
|
||||
margin-left: -1.35em;
|
||||
padding: 1.5em 1.5em 0.5em 2em;
|
||||
border-radius: 0 var(--border-radius) var(--border-radius)
|
||||
var(--border-radius);
|
||||
}
|
||||
|
||||
.current-user-post {
|
||||
.topic-body .cooked {
|
||||
border: 1px solid transparent;
|
||||
background: var(--tertiary-very-low);
|
||||
}
|
||||
.topic-body.highlighted {
|
||||
.cooked {
|
||||
animation: current-user-background-fade-highlight 2.5s ease-out;
|
||||
}
|
||||
}
|
||||
.embedded-posts {
|
||||
.topic-body .cooked {
|
||||
border: 1px solid var(--primary-low);
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.moderator {
|
||||
.topic-body .cooked {
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.deleted .topic-body {
|
||||
.cooked {
|
||||
background: var(--danger-low);
|
||||
}
|
||||
}
|
||||
|
||||
.whisper {
|
||||
.topic-body .cooked {
|
||||
background: transparent;
|
||||
border: 2px dashed var(--primary-low);
|
||||
}
|
||||
|
||||
&.my-post .topic-body .cooked {
|
||||
border: 2px dashed var(--tertiary-very-low);
|
||||
}
|
||||
}
|
||||
|
||||
.topic-body.highlighted {
|
||||
animation: none;
|
||||
.cooked {
|
||||
animation: background-fade-highlight 2.5s ease-out;
|
||||
}
|
||||
}
|
||||
|
||||
.topic-avatar,
|
||||
.topic-body {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.post-menu-area {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.small-action-desc.timegap {
|
||||
flex: 0 0 auto;
|
||||
padding: 0 1em;
|
||||
margin-top: -1.75em;
|
||||
margin-left: -1em;
|
||||
background: var(--secondary);
|
||||
max-width: calc(758px - 1.5em);
|
||||
}
|
||||
|
||||
.post-notice {
|
||||
margin-bottom: 1em;
|
||||
border: none;
|
||||
background: var(--primary-very-low);
|
||||
border-radius: var(--border-radius);
|
||||
margin-left: 1.5em;
|
||||
box-sizing: border-box;
|
||||
padding: 1.5em 2em;
|
||||
}
|
||||
|
||||
.topic-map {
|
||||
margin-left: -1.5em;
|
||||
border: none;
|
||||
border-radius: var(--border-radius);
|
||||
padding: 1.25em;
|
||||
|
||||
section {
|
||||
border: none;
|
||||
}
|
||||
.map:not(.map-collapsed) {
|
||||
.avatars {
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
}
|
||||
|
||||
.participants {
|
||||
margin-bottom: 1.5em;
|
||||
.user {
|
||||
border: none;
|
||||
background: var(--primary-low);
|
||||
padding: 2px 4px;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.map:first-of-type .buttons .btn {
|
||||
border: none;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.embedded-posts {
|
||||
border: none;
|
||||
.topic-body {
|
||||
overflow: visible;
|
||||
}
|
||||
.topic-body,
|
||||
.topic-avatar {
|
||||
border: none !important; // overrides some specificity for .bottom
|
||||
}
|
||||
.collapse-down,
|
||||
.collapse-up {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.timeline-replies {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
margin-right: 0.15em;
|
||||
}
|
||||
|
||||
.gap {
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
}
|
||||
@ -251,6 +251,23 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-menu-assistant {
|
||||
min-width: 100%;
|
||||
margin-top: -1em;
|
||||
|
||||
.search-menu-assistant-item {
|
||||
> span {
|
||||
vertical-align: baseline;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.search-item-user .avatar,
|
||||
.search-item-prefix {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.searching {
|
||||
|
||||
@ -1,26 +1,80 @@
|
||||
// styles that apply to the "share" popup when sharing a link to a post or topic
|
||||
// styles that apply to the "share" modal & popup when sharing a link to a post or topic
|
||||
|
||||
.link-share-container {
|
||||
display: flex;
|
||||
button {
|
||||
transition-property: background-color, color; // don't transition outline
|
||||
}
|
||||
input {
|
||||
width: 100%;
|
||||
font-size: var(--font-up-1);
|
||||
margin-bottom: 0;
|
||||
&:focus + button {
|
||||
outline: 1px solid var(--tertiary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.link-share-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
|
||||
button {
|
||||
margin-top: 0.5em;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.sources {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.new-topic {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.alt-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.share-twitter {
|
||||
.d-icon {
|
||||
color: var(--twitter);
|
||||
}
|
||||
}
|
||||
.share-facebook {
|
||||
.d-icon {
|
||||
color: var(--facebook);
|
||||
}
|
||||
}
|
||||
|
||||
// post share popup
|
||||
|
||||
#share-link {
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
z-index: z("dropdown");
|
||||
box-shadow: shadow("card");
|
||||
background-color: var(--secondary);
|
||||
padding: 8px 8px 4px 8px;
|
||||
padding: 0.5em;
|
||||
width: 300px;
|
||||
display: none;
|
||||
&.visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-bottom: 4px;
|
||||
margin-bottom: 0.5em;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
h3 {
|
||||
font-size: $font-0;
|
||||
@ -30,74 +84,166 @@
|
||||
.date {
|
||||
font-weight: normal;
|
||||
color: var(--primary-med-or-secondary-med);
|
||||
}
|
||||
}
|
||||
|
||||
.copy-text {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
margin: 5px 5px 5px 15px;
|
||||
color: var(--success);
|
||||
opacity: 1;
|
||||
transition: opacity 0.25s;
|
||||
font-size: $font-0;
|
||||
&:not(.success) {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
.social-link {
|
||||
margin-right: 8px;
|
||||
font-size: $font-up-4;
|
||||
.d-icon {
|
||||
color: var(--tertiary-or-white);
|
||||
}
|
||||
.d-icon-fab-facebook {
|
||||
// Adheres to Facebook brand guidelines
|
||||
color: var(--facebook-or-white);
|
||||
}
|
||||
.d-icon-fab-twitter-square {
|
||||
// Adheres to Twitter brand guidelines
|
||||
color: var(--twitter-or-white);
|
||||
}
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
font-size: $font-up-1;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
margin-top: 8px;
|
||||
|
||||
.sources {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
max-width: 45%;
|
||||
|
||||
.social-link {
|
||||
margin-right: 8px;
|
||||
}
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
|
||||
.alt-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
justify-content: flex-end;
|
||||
height: 36px;
|
||||
|
||||
.close-share {
|
||||
font-size: $font-up-3;
|
||||
color: var(--primary-med-or-secondary-med);
|
||||
}
|
||||
|
||||
.new-topic {
|
||||
margin-right: 16px;
|
||||
}
|
||||
.btn.close {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// topic share modal
|
||||
|
||||
.share-topic-modal {
|
||||
form {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.invite-users {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 1.5em 0 0.25em;
|
||||
width: 100%;
|
||||
|
||||
button {
|
||||
margin-top: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.notify-user-input {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
|
||||
.select-kit {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.multi-select-header {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.select-kit.multi-select .choices {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.select-kit.multi-select.is-expanded + button {
|
||||
outline: 1px solid var(--tertiary); // outline the button when the input is outlined, to match height
|
||||
}
|
||||
}
|
||||
|
||||
// topic invite modal
|
||||
|
||||
.create-invite-modal {
|
||||
form {
|
||||
margin: 0;
|
||||
input[type="text"],
|
||||
.btn,
|
||||
.select-kit-header {
|
||||
height: 2.27rem;
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
margin-right: 0.5em;
|
||||
.d-icon {
|
||||
color: var(--primary-medium);
|
||||
margin-right: 0.75em;
|
||||
}
|
||||
}
|
||||
|
||||
textarea {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.input-group:not(:last-of-type) {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
.input-group.input-expires-at,
|
||||
.input-group.input-email,
|
||||
.input-group.invite-max-redemptions {
|
||||
input[type="text"] {
|
||||
width: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.existing-topic,
|
||||
p {
|
||||
// p is for "no topics found"
|
||||
margin-left: 1.75em;
|
||||
margin-top: 0.25em;
|
||||
}
|
||||
|
||||
.future-date-input {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
|
||||
.control-group:nth-child(1) {
|
||||
grid-column-start: 1;
|
||||
grid-column-end: 3;
|
||||
}
|
||||
.control-group:nth-child(2) {
|
||||
margin-left: 1.75em;
|
||||
margin-right: 1.5em;
|
||||
}
|
||||
.control-group:nth-child(2),
|
||||
.control-group:nth-child(3) {
|
||||
grid-row-start: 2;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-bottom: 0;
|
||||
input {
|
||||
height: 100%;
|
||||
margin-bottom: 0;
|
||||
margin-left: 0.5em;
|
||||
width: 150px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input-group.input-email {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
label {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
.invite-email-container {
|
||||
flex: 1 1 auto;
|
||||
#invite-email {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.invite-max-redemptions {
|
||||
label {
|
||||
display: inline;
|
||||
}
|
||||
input {
|
||||
width: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
.show-advanced {
|
||||
margin-left: auto;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
textarea#invite-message,
|
||||
&.invite-to-topic input[type="text"],
|
||||
.group-chooser,
|
||||
.user-chooser,
|
||||
.future-date-input-selector {
|
||||
margin-left: 1.75em;
|
||||
width: calc(100% - 1.75em);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -206,7 +206,6 @@ $quote-share-maxwidth: 150px;
|
||||
.moderator {
|
||||
.regular > .cooked {
|
||||
background-color: var(--highlight-low-or-medium);
|
||||
padding: 10px;
|
||||
}
|
||||
.clearfix > .topic-meta-data > .names {
|
||||
span.user-title {
|
||||
@ -235,6 +234,53 @@ $quote-share-maxwidth: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
.deleted {
|
||||
.regular > .cooked {
|
||||
background-color: var(--danger-low-mid);
|
||||
}
|
||||
.topic-meta-data:not(.embedded-reply) {
|
||||
color: var(--danger);
|
||||
|
||||
.post-info a,
|
||||
a {
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
.d-icon {
|
||||
color: currentColor;
|
||||
}
|
||||
}
|
||||
nav.post-controls {
|
||||
color: var(--danger);
|
||||
|
||||
.show-replies,
|
||||
button.reply.create {
|
||||
color: var(--danger);
|
||||
.d-icon {
|
||||
color: var(--danger);
|
||||
}
|
||||
}
|
||||
.widget-button {
|
||||
&:hover {
|
||||
color: currentColor;
|
||||
background: var(--danger-low);
|
||||
.d-icon {
|
||||
color: currentColor;
|
||||
}
|
||||
}
|
||||
&.fade-out {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.d-icon {
|
||||
color: var(--danger);
|
||||
}
|
||||
}
|
||||
.post-action {
|
||||
color: var(--danger);
|
||||
}
|
||||
}
|
||||
|
||||
// we use aside to hold expandable quotes (versus, say, static blockquotes)
|
||||
aside.quote {
|
||||
margin-top: 1em;
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
@import "ignored-user-list";
|
||||
@import "keyboard_shortcuts";
|
||||
@import "navs";
|
||||
@import "pick-files-button";
|
||||
@import "relative-time-picker";
|
||||
@import "share-and-invite-modal";
|
||||
@import "svg";
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
.pick-files-button {
|
||||
input[type="file"] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@ -2,11 +2,6 @@
|
||||
.modal-body {
|
||||
max-width: 475px;
|
||||
min-width: 320px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.modal-panel {
|
||||
padding: 0.667em;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
|
||||
@ -237,6 +237,7 @@ $avatar_margin: -50px; // negative margin makes avatars extend above cards
|
||||
}
|
||||
.user-card-badge-link,
|
||||
.more-user-badges {
|
||||
vertical-align: top;
|
||||
display: inline-block;
|
||||
}
|
||||
.more-user-badges a {
|
||||
|
||||
@ -23,6 +23,7 @@ $secondary-high: dark-light-diff($secondary, $primary, 30%, -35%) !default;
|
||||
$secondary-very-high: dark-light-diff($secondary, $primary, 7%, -7%) !default;
|
||||
|
||||
//tertiary
|
||||
$tertiary-very-low: dark-light-diff($tertiary, $secondary, 90%, -75%) !default;
|
||||
$tertiary-low: dark-light-diff($tertiary, $secondary, 85%, -65%) !default;
|
||||
$tertiary-medium: dark-light-diff($tertiary, $secondary, 50%, -45%) !default;
|
||||
$tertiary-high: dark-light-diff($tertiary, $secondary, 20%, -25%) !default;
|
||||
|
||||
@ -85,16 +85,17 @@
|
||||
}
|
||||
|
||||
.composer-popup {
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
width: calc(50% - 45px);
|
||||
width: calc(50% - 30px);
|
||||
max-width: 724px;
|
||||
top: 20px;
|
||||
top: 21px; // grippie height + .reply-to margin-top + .reply-area padding-top
|
||||
bottom: 10px;
|
||||
left: 51%;
|
||||
overflow-y: auto;
|
||||
z-index: z("composer", "popover");
|
||||
padding: 10px 10px 35px 10px;
|
||||
box-shadow: shadow("card");
|
||||
box-shadow: shadow("dropdown");
|
||||
background: var(--highlight-medium);
|
||||
.hide-preview & {
|
||||
z-index: z("composer", "dropdown") + 1;
|
||||
@ -108,6 +109,18 @@
|
||||
background-color: var(--tertiary-low);
|
||||
}
|
||||
|
||||
&.dominating-topic-message {
|
||||
bottom: unset;
|
||||
padding: 2.25em 6em 2.5em 2.25em;
|
||||
p {
|
||||
margin-top: 0;
|
||||
font-size: var(--font-up-1);
|
||||
}
|
||||
button {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
@ -523,12 +523,6 @@ video {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.deleted {
|
||||
.topic-body {
|
||||
background-color: var(--danger-low-mid);
|
||||
}
|
||||
}
|
||||
|
||||
.post-select {
|
||||
float: right;
|
||||
margin-right: 20px;
|
||||
|
||||
@ -21,6 +21,7 @@
|
||||
@import "login";
|
||||
@import "menu-panel";
|
||||
@import "modal";
|
||||
@import "personal-message";
|
||||
@import "push-notifications-mobile";
|
||||
@import "reviewables";
|
||||
@import "ring";
|
||||
|
||||
58
app/assets/stylesheets/mobile/personal-message.scss
Normal file
58
app/assets/stylesheets/mobile/personal-message.scss
Normal file
@ -0,0 +1,58 @@
|
||||
.archetype-private_message {
|
||||
.topic-body .cooked {
|
||||
margin-top: 0.5em;
|
||||
margin-left: 0;
|
||||
padding: 1.5em 1em;
|
||||
border-radius: 0.75em var(--border-radius) var(--border-radius)
|
||||
var(--border-radius);
|
||||
}
|
||||
.topic-avatar {
|
||||
margin-bottom: -1em; // creates bubble overlap
|
||||
}
|
||||
.boxed .contents {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.topic-post {
|
||||
margin: 0 0 1em;
|
||||
article {
|
||||
border-top: none;
|
||||
}
|
||||
}
|
||||
|
||||
.topic-body {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.topic-map {
|
||||
padding: 1em 0.5em;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.post-notice {
|
||||
padding: 1em;
|
||||
margin: 0 0 1em;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.small-action {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.small-action-desc.timegap {
|
||||
margin-left: 0;
|
||||
padding: 1em 1em 1em 0;
|
||||
}
|
||||
|
||||
.small-action:not(.time-gap) {
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.topic-meta-data .names .first.staff {
|
||||
flex-basis: 100%;
|
||||
& + .second,
|
||||
& + .second + .user-title {
|
||||
flex-basis: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -87,11 +87,7 @@ span.badge-posts {
|
||||
&.expand-post {
|
||||
margin: 10px 0 10px 0;
|
||||
}
|
||||
&.reply {
|
||||
.d-icon {
|
||||
color: var(--primary-high);
|
||||
}
|
||||
}
|
||||
|
||||
&.has-like {
|
||||
.d-icon {
|
||||
color: var(--love);
|
||||
@ -126,6 +122,10 @@ span.badge-posts {
|
||||
}
|
||||
}
|
||||
|
||||
nav.post-controls button.reply .d-icon {
|
||||
color: var(--primary-high);
|
||||
}
|
||||
|
||||
.post-admin-menu {
|
||||
bottom: -50px;
|
||||
left: 135px;
|
||||
@ -337,12 +337,6 @@ button.select-post {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.deleted {
|
||||
.topic-body {
|
||||
background-color: var(--danger-low-mid);
|
||||
}
|
||||
}
|
||||
|
||||
.deleted-user-avatar {
|
||||
font-size: $font-up-5;
|
||||
}
|
||||
|
||||
@ -302,6 +302,7 @@ class CategoriesController < ApplicationController
|
||||
:mailinglist_mirror,
|
||||
:all_topics_wiki,
|
||||
:allow_unlimited_owner_edits_on_first_post,
|
||||
:default_slow_mode_seconds,
|
||||
:parent_category_id,
|
||||
:auto_close_hours,
|
||||
:auto_close_based_on_last_post,
|
||||
|
||||
@ -5,14 +5,12 @@ class EmbedController < ApplicationController
|
||||
|
||||
skip_before_action :check_xhr, :preload_json, :verify_authenticity_token
|
||||
|
||||
before_action :ensure_embeddable, except: [ :info, :topics ]
|
||||
before_action :prepare_embeddable, except: [ :info ]
|
||||
before_action :ensure_api_request, only: [ :info ]
|
||||
|
||||
layout 'embed'
|
||||
|
||||
rescue_from Discourse::InvalidAccess do
|
||||
response.headers.delete('X-Frame-Options')
|
||||
if current_user.try(:admin?)
|
||||
@setup_url = "#{Discourse.base_url}/admin/customize/embedding"
|
||||
@show_reason = true
|
||||
@ -24,7 +22,6 @@ class EmbedController < ApplicationController
|
||||
def topics
|
||||
discourse_expires_in 1.minute
|
||||
|
||||
response.headers.delete('X-Frame-Options')
|
||||
unless SiteSetting.embed_topics_list?
|
||||
render 'embed_topics_error', status: 400
|
||||
return
|
||||
@ -73,6 +70,11 @@ class EmbedController < ApplicationController
|
||||
def comments
|
||||
embed_url = params[:embed_url]
|
||||
embed_username = params[:discourse_username]
|
||||
embed_topic_id = params[:topic_id]&.to_i
|
||||
|
||||
unless embed_topic_id || EmbeddableHost.url_allowed?(embed_url)
|
||||
raise Discourse::InvalidAccess.new('invalid embed host')
|
||||
end
|
||||
|
||||
topic_id = nil
|
||||
if embed_url.present?
|
||||
@ -147,6 +149,7 @@ class EmbedController < ApplicationController
|
||||
private
|
||||
|
||||
def prepare_embeddable
|
||||
response.headers.delete('X-Frame-Options')
|
||||
@embeddable_css_class = ""
|
||||
embeddable_host = EmbeddableHost.record_for_url(request.referer)
|
||||
@embeddable_css_class = " class=\"#{embeddable_host.class_name}\"" if embeddable_host.present? && embeddable_host.class_name.present?
|
||||
@ -158,19 +161,4 @@ class EmbedController < ApplicationController
|
||||
def ensure_api_request
|
||||
raise Discourse::InvalidAccess.new('api key not set') if !is_api?
|
||||
end
|
||||
|
||||
def ensure_embeddable
|
||||
if !(Rails.env.development? && current_user&.admin?)
|
||||
referer = request.referer
|
||||
|
||||
unless referer && EmbeddableHost.url_allowed?(referer)
|
||||
raise Discourse::InvalidAccess.new('invalid referer host')
|
||||
end
|
||||
end
|
||||
|
||||
response.headers.delete('X-Frame-Options')
|
||||
rescue URI::Error
|
||||
raise Discourse::InvalidAccess.new('invalid referer host')
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@ -333,19 +333,9 @@ class GroupsController < ApplicationController
|
||||
|
||||
def add_members
|
||||
group = Group.find(params[:id])
|
||||
group.public_admission ? ensure_logged_in : guardian.ensure_can_edit!(group)
|
||||
guardian.ensure_can_edit!(group)
|
||||
|
||||
users = users_from_params.to_a
|
||||
|
||||
if group.public_admission
|
||||
if !guardian.can_log_group_changes?(group) && current_user != users.first
|
||||
raise Discourse::InvalidAccess
|
||||
end
|
||||
|
||||
unless current_user.staff?
|
||||
RateLimiter.new(current_user, "public_group_membership", 3, 1.minute).performed!
|
||||
end
|
||||
end
|
||||
|
||||
emails = []
|
||||
if params[:emails]
|
||||
params[:emails].split(",").each do |email|
|
||||
@ -376,17 +366,10 @@ class GroupsController < ApplicationController
|
||||
count: usernames_already_in_group.size
|
||||
))
|
||||
else
|
||||
notify = params[:notify_users]&.to_s == "true"
|
||||
uniq_users = users.uniq
|
||||
uniq_users.each do |user|
|
||||
begin
|
||||
group.add(user)
|
||||
GroupActionLogger.new(current_user, group).log_add_user_to_group(user)
|
||||
group.notify_added_to_group(user) if params[:notify_users]&.to_s == "true"
|
||||
rescue ActiveRecord::RecordNotUnique
|
||||
# Under concurrency, we might attempt to insert two records quickly and hit a DB
|
||||
# constraint. In this case we can safely ignore the error and act as if the user
|
||||
# was added to the group.
|
||||
end
|
||||
add_user_to_group(group, user, notify)
|
||||
end
|
||||
|
||||
emails.each do |email|
|
||||
@ -408,6 +391,20 @@ class GroupsController < ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
def join
|
||||
ensure_logged_in
|
||||
unless current_user.staff?
|
||||
RateLimiter.new(current_user, "public_group_membership", 3, 1.minute).performed!
|
||||
end
|
||||
|
||||
group = Group.find(params[:id])
|
||||
raise Discourse::NotFound unless group
|
||||
raise Discourse::InvalidAccess unless group.public_admission
|
||||
|
||||
return if group.users.exists?(id: current_user.id)
|
||||
add_user_to_group(group, current_user)
|
||||
end
|
||||
|
||||
def handle_membership_request
|
||||
group = Group.find_by(id: params[:id])
|
||||
raise Discourse::InvalidParameters.new(:id) if group.blank?
|
||||
@ -467,7 +464,7 @@ class GroupsController < ApplicationController
|
||||
def remove_member
|
||||
group = Group.find_by(id: params[:id])
|
||||
raise Discourse::NotFound unless group
|
||||
group.public_exit ? ensure_logged_in : guardian.ensure_can_edit!(group)
|
||||
guardian.ensure_can_edit!(group)
|
||||
|
||||
# Maintain backwards compatibility
|
||||
params[:usernames] = params[:username] if params[:username].present?
|
||||
@ -478,16 +475,6 @@ class GroupsController < ApplicationController
|
||||
'user_ids or usernames or user_emails must be present'
|
||||
) if users.empty?
|
||||
|
||||
if group.public_exit
|
||||
if !guardian.can_log_group_changes?(group) && current_user != users.first
|
||||
raise Discourse::InvalidAccess
|
||||
end
|
||||
|
||||
unless current_user.staff?
|
||||
RateLimiter.new(current_user, "public_group_membership", 3, 1.minute).performed!
|
||||
end
|
||||
end
|
||||
|
||||
removed_users = []
|
||||
skipped_users = []
|
||||
|
||||
@ -510,6 +497,21 @@ class GroupsController < ApplicationController
|
||||
)
|
||||
end
|
||||
|
||||
def leave
|
||||
ensure_logged_in
|
||||
unless current_user.staff?
|
||||
RateLimiter.new(current_user, "public_group_membership", 3, 1.minute).performed!
|
||||
end
|
||||
|
||||
group = Group.find_by(id: params[:id])
|
||||
raise Discourse::NotFound unless group
|
||||
raise Discourse::InvalidAccess unless group.public_exit
|
||||
|
||||
if group.remove(current_user)
|
||||
GroupActionLogger.new(current_user, group).log_remove_user_from_group(current_user)
|
||||
end
|
||||
end
|
||||
|
||||
MAX_NOTIFIED_OWNERS ||= 20
|
||||
|
||||
def request_membership
|
||||
@ -650,6 +652,16 @@ class GroupsController < ApplicationController
|
||||
|
||||
private
|
||||
|
||||
def add_user_to_group(group, user, notify = false)
|
||||
group.add(user)
|
||||
GroupActionLogger.new(current_user, group).log_add_user_to_group(user)
|
||||
group.notify_added_to_group(user) if notify
|
||||
rescue ActiveRecord::RecordNotUnique
|
||||
# Under concurrency, we might attempt to insert two records quickly and hit a DB
|
||||
# constraint. In this case we can safely ignore the error and act as if the user
|
||||
# was added to the group.
|
||||
end
|
||||
|
||||
def group_params(automatic: false)
|
||||
permitted_params =
|
||||
if automatic
|
||||
|
||||
@ -50,10 +50,13 @@ class InvitesController < ApplicationController
|
||||
email = Email.obfuscate(invite.email)
|
||||
|
||||
# Show email if the user already authenticated their email
|
||||
different_external_email = false
|
||||
if session[:authentication]
|
||||
auth_result = Auth::Result.from_session_data(session[:authentication], user: nil)
|
||||
if invite.email == auth_result.email
|
||||
email = invite.email
|
||||
else
|
||||
different_external_email = true
|
||||
end
|
||||
end
|
||||
|
||||
@ -73,6 +76,10 @@ class InvitesController < ApplicationController
|
||||
email_verified_by_link: email_verified_by_link
|
||||
}
|
||||
|
||||
if different_external_email
|
||||
info[:different_external_email] = true
|
||||
end
|
||||
|
||||
if staged_user = User.where(staged: true).with_email(invite.email).first
|
||||
info[:username] = staged_user.username
|
||||
info[:user_fields] = staged_user.user_fields
|
||||
|
||||
@ -10,6 +10,7 @@ class QunitController < ApplicationController
|
||||
|
||||
# only used in test / dev
|
||||
def index
|
||||
raise Discourse::NotFound.new if request.headers["HTTP_X_DISCOURSE_EMBER_CLI"] == "true"
|
||||
raise Discourse::InvalidAccess.new if Rails.env.production?
|
||||
end
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user