Version bump

This commit is contained in:
Neil Lalonde 2021-07-22 12:27:27 -04:00
commit 93e91879c7
No known key found for this signature in database
GPG Key ID: FF871CA9037D0A91
348 changed files with 13068 additions and 5286 deletions

View File

@ -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

View File

@ -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)

View File

@ -66,7 +66,7 @@ Discourse supports the **latest, stable releases** of all major browsers and pla
- [Redis](https://redis.io/) &mdash; We use Redis as a cache and for transient data.
- [BrowserStack](https://www.browserstack.com/) &mdash; 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

View File

@ -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 &&

View File

@ -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);
},
});

View File

@ -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|}}

View File

@ -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,

View File

@ -79,7 +79,7 @@ export default Component.extend({
}
},
willDestoryElement() {
willDestroyElement() {
this._tableContainer.removeEventListener("scroll", this.onBottomScroll);
this._topHorizontalScrollBar.removeEventListener(
"scroll",

View File

@ -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);
},
});

View File

@ -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(() => {

View File

@ -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) {

View File

@ -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();
},
});

View File

@ -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`;
}

View File

@ -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);
},

View File

@ -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");

View File

@ -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)

View File

@ -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")

View File

@ -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) {

View File

@ -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.[]")

View File

@ -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"),

View File

@ -1,2 +1,3 @@
import Controller from "@ember/controller";
export default Controller.extend({});

View File

@ -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);
},
};

View File

@ -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) {

View File

@ -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

View File

@ -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 }),
})
);
}
}

View File

@ -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));

View File

@ -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,

View File

@ -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",

View File

@ -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);
});
},
});

View File

@ -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",

View File

@ -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);
}
});
}

View File

@ -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);
},
});

View File

@ -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;
}

View File

@ -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}}

View File

@ -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">

View File

@ -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}}

View File

@ -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">

View File

@ -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>

View File

@ -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)
}}

View File

@ -1 +1,3 @@
{{yield info}}
{{#if canRender}}
{{yield info}}
{{/if}}

View File

@ -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)}}

View File

@ -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>

View File

@ -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}}

View File

@ -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|}}

View File

@ -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

View File

@ -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}}

View File

@ -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"}}

View File

@ -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}}

View File

@ -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,

View File

@ -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();
});
}
},

View File

@ -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");
}

View File

@ -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;

View File

@ -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;
},
});

View File

@ -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;

View File

@ -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) {

View File

@ -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"
);
});
}
);

View File

@ -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);
});
});

View File

@ -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} `);
});
});

View File

@ -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']"),

View File

@ -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

View File

@ -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,

View File

@ -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",
},
},
};

View File

@ -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);
},
}
);
}
);

View File

@ -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"
);
},
}
);
});

View File

@ -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";

View File

@ -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"],

View File

@ -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 ||

View File

@ -0,0 +1,8 @@
let ajax;
if (window.Discourse) {
ajax = requirejs("discourse/lib/ajax").ajax;
} else {
ajax = requirejs("wizard/lib/ajax").ajax;
}
export { ajax };

View File

@ -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";

View File

@ -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}}

View File

@ -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)

View File

@ -1,5 +1,5 @@
{{#each icons as |icon|}}
{{d-icon icon translatedtitle=(dasherize title)}}
{{d-icon icon translatedtitle=dasherizedTitle}}
{{/each}}
<span class="name">

View File

@ -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);
});
}

View File

@ -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};

View File

@ -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 {

View File

@ -33,6 +33,7 @@
@import "modal";
@import "not-found";
@import "onebox";
@import "personal-message";
@import "popup-menu";
@import "redirection";
@import "request_access";

View File

@ -322,7 +322,7 @@
display: flex;
align-items: center;
margin-right: auto;
button {
.btn-primary {
flex: 0 0 auto;
}
.cancel {

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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;

View 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;
}
}

View File

@ -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 {

View File

@ -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);
}
}
}

View File

@ -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;

View File

@ -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";

View File

@ -0,0 +1,5 @@
.pick-files-button {
input[type="file"] {
display: none;
}
}

View File

@ -2,11 +2,6 @@
.modal-body {
max-width: 475px;
min-width: 320px;
padding: 0;
}
.modal-panel {
padding: 0.667em;
}
.modal-header {

View File

@ -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 {

View File

@ -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;

View File

@ -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;
}

View File

@ -523,12 +523,6 @@ video {
position: relative;
}
.deleted {
.topic-body {
background-color: var(--danger-low-mid);
}
}
.post-select {
float: right;
margin-right: 20px;

View File

@ -21,6 +21,7 @@
@import "login";
@import "menu-panel";
@import "modal";
@import "personal-message";
@import "push-notifications-mobile";
@import "reviewables";
@import "ring";

View 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;
}
}
}

View File

@ -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;
}

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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