Merge remote-tracking branch 'origin/main' into generic-import
This commit is contained in:
commit
c0ad2adeef
22
Gemfile.lock
22
Gemfile.lock
@ -129,10 +129,10 @@ GEM
|
||||
sprockets (>= 3.3, < 4.1)
|
||||
ember-source (2.18.2)
|
||||
erubi (1.10.0)
|
||||
excon (0.88.0)
|
||||
excon (0.89.0)
|
||||
execjs (2.8.1)
|
||||
exifr (1.3.9)
|
||||
fabrication (2.22.0)
|
||||
fabrication (2.23.0)
|
||||
faker (2.19.0)
|
||||
i18n (>= 1.6, < 2)
|
||||
fakeweb (1.3.0)
|
||||
@ -215,8 +215,8 @@ GEM
|
||||
logstash-event (1.2.02)
|
||||
logstash-logger (0.26.1)
|
||||
logstash-event (~> 1.2)
|
||||
logster (2.10.0)
|
||||
loofah (2.12.0)
|
||||
logster (2.10.1)
|
||||
loofah (2.13.0)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.5.9)
|
||||
lru_redux (1.1.0)
|
||||
@ -233,7 +233,7 @@ GEM
|
||||
mini_scheduler (0.13.0)
|
||||
sidekiq (>= 4.2.3)
|
||||
mini_sql (1.1.3)
|
||||
mini_suffix (0.3.2)
|
||||
mini_suffix (0.3.3)
|
||||
ffi (~> 1.9)
|
||||
minitest (5.14.4)
|
||||
mocha (1.13.0)
|
||||
@ -292,7 +292,7 @@ GEM
|
||||
parallel (1.21.0)
|
||||
parallel_tests (3.7.3)
|
||||
parallel
|
||||
parser (3.0.3.1)
|
||||
parser (3.0.3.2)
|
||||
ast (~> 2.4.1)
|
||||
pg (1.2.3)
|
||||
progress (3.6.0)
|
||||
@ -335,7 +335,7 @@ GEM
|
||||
rake (>= 0.13)
|
||||
thor (~> 1.0)
|
||||
rainbow (3.0.0)
|
||||
raindrops (0.19.2)
|
||||
raindrops (0.20.0)
|
||||
rake (13.0.6)
|
||||
rb-fsevent (0.11.0)
|
||||
rb-inotify (0.10.1)
|
||||
@ -349,7 +349,7 @@ GEM
|
||||
redis (4.5.1)
|
||||
redis-namespace (1.8.1)
|
||||
redis (>= 3.0.4)
|
||||
regexp_parser (2.1.1)
|
||||
regexp_parser (2.2.0)
|
||||
request_store (1.5.0)
|
||||
rack (>= 1.4)
|
||||
rexml (3.2.5)
|
||||
@ -399,9 +399,9 @@ GEM
|
||||
rubocop-ast (>= 1.12.0, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 1.4.0, < 3.0)
|
||||
rubocop-ast (1.13.0)
|
||||
rubocop-ast (1.15.0)
|
||||
parser (>= 3.0.1.1)
|
||||
rubocop-discourse (2.4.2)
|
||||
rubocop-discourse (2.5.0)
|
||||
rubocop (>= 1.1.0)
|
||||
rubocop-rspec (>= 2.0.0)
|
||||
rubocop-rspec (2.6.0)
|
||||
@ -443,7 +443,7 @@ GEM
|
||||
sprockets (3.7.2)
|
||||
concurrent-ruby (~> 1.0)
|
||||
rack (> 1, < 3)
|
||||
sprockets-rails (3.4.1)
|
||||
sprockets-rails (3.4.2)
|
||||
actionpack (>= 5.2)
|
||||
activesupport (>= 5.2)
|
||||
sprockets (>= 3.0.0)
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { action } from "@ember/object";
|
||||
import Controller from "@ember/controller";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
|
||||
@ -13,13 +14,11 @@ export default Controller.extend({
|
||||
.compact();
|
||||
},
|
||||
|
||||
actions: {
|
||||
clearFilter() {
|
||||
this.setProperties({ filter: "", onlyOverridden: false });
|
||||
},
|
||||
|
||||
toggleMenu() {
|
||||
$(".admin-detail").toggleClass("mobile-closed mobile-open");
|
||||
},
|
||||
@action
|
||||
toggleMenu() {
|
||||
const adminDetail = document.querySelector(".admin-detail");
|
||||
["mobile-closed", "mobile-open"].forEach((state) => {
|
||||
adminDetail.classList.toggle(state);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@ -5,6 +5,7 @@ import { alias } from "@ember/object/computed";
|
||||
import discourseDebounce from "discourse-common/lib/debounce";
|
||||
import { isEmpty } from "@ember/utils";
|
||||
import { observes } from "discourse-common/utils/decorators";
|
||||
import { action } from "@ember/object";
|
||||
|
||||
export default Controller.extend({
|
||||
filter: null,
|
||||
@ -126,13 +127,16 @@ export default Controller.extend({
|
||||
);
|
||||
},
|
||||
|
||||
actions: {
|
||||
clearFilter() {
|
||||
this.setProperties({ filter: "", onlyOverridden: false });
|
||||
},
|
||||
@action
|
||||
clearFilter() {
|
||||
this.setProperties({ filter: "", onlyOverridden: false });
|
||||
},
|
||||
|
||||
toggleMenu() {
|
||||
$(".admin-detail").toggleClass("mobile-closed mobile-open");
|
||||
},
|
||||
@action
|
||||
toggleMenu() {
|
||||
const adminDetail = document.querySelector(".admin-detail");
|
||||
["mobile-closed", "mobile-open"].forEach((state) => {
|
||||
adminDetail.classList.toggle(state);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@ -50,6 +50,9 @@ export default Controller.extend({
|
||||
|
||||
@action
|
||||
toggleMenu() {
|
||||
$(".admin-detail").toggleClass("mobile-closed mobile-open");
|
||||
const adminDetail = document.querySelector(".admin-detail");
|
||||
["mobile-closed", "mobile-open"].forEach((state) => {
|
||||
adminDetail.classList.toggle(state);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
class="btn-default"}}
|
||||
{{/if}}
|
||||
{{else}}
|
||||
{{#if (and siteSettings.enable_direct_s3_uploads siteSettings.enable_experimental_backup_uploader)}}
|
||||
{{#if siteSettings.enable_experimental_backup_uploader}}
|
||||
{{uppy-backup-uploader done=(route-action "remoteUploadSuccess")}}
|
||||
{{else}}
|
||||
{{backup-uploader done=(route-action "remoteUploadSuccess")}}
|
||||
|
||||
@ -26,5 +26,3 @@
|
||||
<div class="admin-detail pull-left mobile-closed">
|
||||
{{outlet}}
|
||||
</div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
@ -19,7 +19,13 @@
|
||||
"ember-cli-htmlbars": "^4.2.0",
|
||||
"ember-auto-import": "^1.5.3",
|
||||
"handlebars": "^4.7.0",
|
||||
"truth-helpers": "^1.0.0"
|
||||
"truth-helpers": "^1.0.0",
|
||||
"@uppy/aws-s3": "^2.0.4",
|
||||
"@uppy/aws-s3-multipart": "^2.1.0",
|
||||
"@uppy/core": "^2.1.0",
|
||||
"@uppy/drop-target": "^1.1.0",
|
||||
"@uppy/utils": "^4.0.3",
|
||||
"@uppy/xhr-upload": "^2.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ember/optional-features": "^1.1.0",
|
||||
|
||||
@ -15,21 +15,19 @@ export default Component.extend({
|
||||
return;
|
||||
}
|
||||
const slug = this.get("category.fullSlug");
|
||||
const tags = this.tags;
|
||||
|
||||
this._removeClass();
|
||||
|
||||
let classes = [];
|
||||
const classes = [];
|
||||
|
||||
if (slug) {
|
||||
classes.push("category");
|
||||
classes.push(`category-${slug}`);
|
||||
}
|
||||
if (tags) {
|
||||
tags.forEach((t) => classes.push(`tag-${t}`));
|
||||
}
|
||||
if (classes.length > 0) {
|
||||
$("body").addClass(classes.join(" "));
|
||||
}
|
||||
|
||||
this.tags?.forEach((t) => classes.push(`tag-${t}`));
|
||||
|
||||
document.body.classList.add(...classes);
|
||||
},
|
||||
|
||||
@observes("category.fullSlug", "tags")
|
||||
@ -37,14 +35,23 @@ export default Component.extend({
|
||||
scheduleOnce("afterRender", this, this._updateClass);
|
||||
},
|
||||
|
||||
_removeClass() {
|
||||
$("body").removeClass((_, css) =>
|
||||
(css.match(/\b(?:category|tag)-\S+|( category )/g) || []).join(" ")
|
||||
);
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
this._super(...arguments);
|
||||
this._removeClass();
|
||||
},
|
||||
|
||||
_removeClass() {
|
||||
const invalidClasses = [];
|
||||
const regex = /\b(?:category|tag)-\S+|( category )/g;
|
||||
|
||||
document.body.classList.forEach((name) => {
|
||||
if (regex.test(name)) {
|
||||
invalidClasses.push(name);
|
||||
}
|
||||
});
|
||||
|
||||
if (invalidClasses.length) {
|
||||
document.body.classList.remove(...invalidClasses);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import discourseComputed, { observes } from "discourse-common/utils/decorators";
|
||||
import Component from "@ember/component";
|
||||
import { action } from "@ember/object";
|
||||
import discourseDebounce from "discourse-common/lib/debounce";
|
||||
import { isEmpty } from "@ember/utils";
|
||||
import { next, schedule } from "@ember/runloop";
|
||||
import { searchForTerm } from "discourse/lib/search";
|
||||
import { INPUT_DELAY } from "discourse-common/config/environment";
|
||||
|
||||
@ -26,7 +26,7 @@ export default Component.extend({
|
||||
|
||||
if (this.loadOnInit && !isEmpty(this.additionalFilters)) {
|
||||
searchForTerm(this.additionalFilters, {}).then((results) => {
|
||||
if (results && results.posts && results.posts.length > 0) {
|
||||
if (results?.posts?.length > 0) {
|
||||
this.set(
|
||||
"topics",
|
||||
results.posts
|
||||
@ -42,18 +42,18 @@ export default Component.extend({
|
||||
|
||||
didInsertElement() {
|
||||
this._super(...arguments);
|
||||
schedule("afterRender", () => {
|
||||
$("#choose-topic-title").keydown((e) => {
|
||||
if (e.key === "Enter") {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document
|
||||
.getElementById("choose-topic-title")
|
||||
.addEventListener("keydown", this._handleEnter);
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
this._super(...arguments);
|
||||
$("#choose-topic-title").off("keydown");
|
||||
|
||||
document
|
||||
.getElementById("choose-topic-title")
|
||||
.removeEventListener("keydown", this._handleEnter);
|
||||
},
|
||||
|
||||
@observes("topicTitle")
|
||||
@ -102,7 +102,7 @@ export default Component.extend({
|
||||
|
||||
const currentTopicId = this.currentTopicId;
|
||||
const titleWithFilters = `${title} ${this.additionalFilters}`;
|
||||
let searchParams = {};
|
||||
const searchParams = {};
|
||||
|
||||
if (!isEmpty(title)) {
|
||||
searchParams.typeFilter = "topic";
|
||||
@ -116,7 +116,7 @@ export default Component.extend({
|
||||
if (title !== this.topicTitle) {
|
||||
return;
|
||||
}
|
||||
if (results && results.posts && results.posts.length > 0) {
|
||||
if (results?.posts?.length > 0) {
|
||||
this.set(
|
||||
"topics",
|
||||
results.posts.mapBy("topic").filter((t) => t.id !== currentTopicId)
|
||||
@ -130,15 +130,18 @@ export default Component.extend({
|
||||
});
|
||||
},
|
||||
|
||||
actions: {
|
||||
chooseTopic(topic) {
|
||||
this.set("selectedTopicId", topic.id);
|
||||
next(() => {
|
||||
document.getElementById(`choose-topic-${topic.id}`).checked = true;
|
||||
});
|
||||
if (this.topicChangedCallback) {
|
||||
this.topicChangedCallback(topic);
|
||||
}
|
||||
},
|
||||
@action
|
||||
chooseTopic(topic) {
|
||||
this.set("selectedTopicId", topic.id);
|
||||
|
||||
if (this.topicChangedCallback) {
|
||||
this.topicChangedCallback(topic);
|
||||
}
|
||||
},
|
||||
|
||||
_handleEnter(event) {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@ -285,6 +285,9 @@ export default Component.extend(TextareaTextManipulation, {
|
||||
});
|
||||
});
|
||||
|
||||
this._itsatrap.bind("tab", () => this._indentSelection("right"));
|
||||
this._itsatrap.bind("shift+tab", () => this._indentSelection("left"));
|
||||
|
||||
// disable clicking on links in the preview
|
||||
this.element
|
||||
.querySelector(".d-editor-preview")
|
||||
@ -294,6 +297,11 @@ export default Component.extend(TextareaTextManipulation, {
|
||||
this.appEvents.on("composer:insert-block", this, "_insertBlock");
|
||||
this.appEvents.on("composer:insert-text", this, "_insertText");
|
||||
this.appEvents.on("composer:replace-text", this, "_replaceText");
|
||||
this.appEvents.on(
|
||||
"composer:indent-selected-text",
|
||||
this,
|
||||
"_indentSelection"
|
||||
);
|
||||
}
|
||||
|
||||
if (isTesting()) {
|
||||
@ -333,6 +341,11 @@ export default Component.extend(TextareaTextManipulation, {
|
||||
this.appEvents.off("composer:insert-block", this, "_insertBlock");
|
||||
this.appEvents.off("composer:insert-text", this, "_insertText");
|
||||
this.appEvents.off("composer:replace-text", this, "_replaceText");
|
||||
this.appEvents.off(
|
||||
"composer:indent-selected-text",
|
||||
this,
|
||||
"_indentSelection"
|
||||
);
|
||||
}
|
||||
|
||||
this._itsatrap?.destroy();
|
||||
|
||||
@ -1,24 +1,35 @@
|
||||
import deprecated from "discourse-common/lib/deprecated";
|
||||
import Component from "@ember/component";
|
||||
import { scrollTop } from "discourse/mixins/scroll-top";
|
||||
|
||||
// Can add a body class from within a component, also will scroll to the top automatically.
|
||||
export default Component.extend({
|
||||
tagName: "section",
|
||||
tagName: null,
|
||||
pageClass: null,
|
||||
bodyClass: null,
|
||||
scrollTop: true,
|
||||
|
||||
didInsertElement() {
|
||||
this._super(...arguments);
|
||||
|
||||
const pageClass = this.pageClass;
|
||||
if (pageClass) {
|
||||
$("body").addClass(`${pageClass}-page`);
|
||||
if (this.pageClass) {
|
||||
document.body.classList.add(`${this.pageClass}-page`);
|
||||
}
|
||||
|
||||
const bodyClass = this.bodyClass;
|
||||
if (bodyClass) {
|
||||
$("body").addClass(bodyClass);
|
||||
if (this.bodyClass) {
|
||||
document.body.classList.add(...this.bodyClass.split(" "));
|
||||
}
|
||||
|
||||
if (this.scrollTop === "false") {
|
||||
deprecated("Uses boolean instead of string for scrollTop.", {
|
||||
since: "2.8.0.beta9",
|
||||
dropFrom: "2.9.0.beta1",
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.scrollTop) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -27,14 +38,13 @@ export default Component.extend({
|
||||
|
||||
willDestroyElement() {
|
||||
this._super(...arguments);
|
||||
const pageClass = this.pageClass;
|
||||
if (pageClass) {
|
||||
$("body").removeClass(`${pageClass}-page`);
|
||||
|
||||
if (this.pageClass) {
|
||||
document.body.classList.remove(`${this.pageClass}-page`);
|
||||
}
|
||||
|
||||
const bodyClass = this.bodyClass;
|
||||
if (bodyClass) {
|
||||
$("body").removeClass(bodyClass);
|
||||
if (this.bodyClass) {
|
||||
document.body.classList.remove(...this.bodyClass.split(" "));
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@ -1,19 +1,20 @@
|
||||
import Component from "@ember/component";
|
||||
import UrlRefresh from "discourse/mixins/url-refresh";
|
||||
import { on } from "discourse-common/utils/decorators";
|
||||
|
||||
const CATEGORIES_LIST_BODY_CLASS = "categories-list";
|
||||
|
||||
export default Component.extend(UrlRefresh, {
|
||||
classNames: ["contents"],
|
||||
|
||||
@on("didInsertElement")
|
||||
addBodyClass() {
|
||||
$("body").addClass(CATEGORIES_LIST_BODY_CLASS);
|
||||
didInsertElement() {
|
||||
this._super(...arguments);
|
||||
|
||||
document.body.classList.add(CATEGORIES_LIST_BODY_CLASS);
|
||||
},
|
||||
|
||||
@on("willDestroyElement")
|
||||
removeBodyClass() {
|
||||
$("body").removeClass(CATEGORIES_LIST_BODY_CLASS);
|
||||
willDestroyElement() {
|
||||
this._super(...arguments);
|
||||
|
||||
document.body.classList.remove(CATEGORIES_LIST_BODY_CLASS);
|
||||
},
|
||||
});
|
||||
|
||||
@ -37,6 +37,7 @@ export default Component.extend({
|
||||
hoveredEmoji: null,
|
||||
isActive: false,
|
||||
isLoading: true,
|
||||
usePopper: true,
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
@ -88,25 +89,24 @@ export default Component.extend({
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.site.isMobileDevice) {
|
||||
this._popper = createPopper(
|
||||
document.querySelector(".d-editor-textarea-wrapper"),
|
||||
emojiPicker,
|
||||
{
|
||||
placement: "auto",
|
||||
modifiers: [
|
||||
{
|
||||
name: "preventOverflow",
|
||||
const textareaWrapper = document.querySelector(
|
||||
".d-editor-textarea-wrapper"
|
||||
);
|
||||
if (!this.site.isMobileDevice && this.usePopper && textareaWrapper) {
|
||||
this._popper = createPopper(textareaWrapper, emojiPicker, {
|
||||
placement: "auto",
|
||||
modifiers: [
|
||||
{
|
||||
name: "preventOverflow",
|
||||
},
|
||||
{
|
||||
name: "offset",
|
||||
options: {
|
||||
offset: [5, 5],
|
||||
},
|
||||
{
|
||||
name: "offset",
|
||||
options: {
|
||||
offset: [5, 5],
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
// this is a low-tech trick to prevent appending hundreds of emojis
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
import Component from "@ember/component";
|
||||
import I18n from "I18n";
|
||||
import { computed } from "@ember/object";
|
||||
import { not } from "@ember/object/computed";
|
||||
import { not, readOnly } from "@ember/object/computed";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import AssociatedGroup from "discourse/models/associated-group";
|
||||
|
||||
export default Component.extend({
|
||||
tokenSeparator: "|",
|
||||
showAssociatedGroups: readOnly("site.can_associate_groups"),
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
@ -20,6 +22,10 @@ export default Component.extend({
|
||||
{ name: 3, value: 3 },
|
||||
{ name: 4, value: 4 },
|
||||
];
|
||||
|
||||
if (this.showAssociatedGroups) {
|
||||
this.loadAssociatedGroups();
|
||||
}
|
||||
},
|
||||
|
||||
canEdit: not("model.automatic"),
|
||||
@ -54,6 +60,10 @@ export default Component.extend({
|
||||
return this.model.emailDomains.split(this.tokenSeparator).filter(Boolean);
|
||||
}),
|
||||
|
||||
loadAssociatedGroups() {
|
||||
AssociatedGroup.list().then((ags) => this.set("associatedGroups", ags));
|
||||
},
|
||||
|
||||
actions: {
|
||||
onChangeEmailDomainsSetting(value) {
|
||||
this.set(
|
||||
|
||||
@ -183,9 +183,13 @@ const SiteHeaderComponent = MountWidget.extend(
|
||||
}
|
||||
|
||||
const offset = info.offset();
|
||||
const headerRect = header.getBoundingClientRect(),
|
||||
headerOffset = headerRect.top + headerRect.height,
|
||||
doc = document.documentElement;
|
||||
const headerRect = header.getBoundingClientRect();
|
||||
const doc = document.documentElement;
|
||||
let headerOffset = headerRect.top + headerRect.height;
|
||||
|
||||
if (window.scrollY < 0) {
|
||||
headerOffset -= window.scrollY;
|
||||
}
|
||||
|
||||
const newValue = `${headerOffset}px`;
|
||||
if (newValue !== this.currentHeaderOffsetValue) {
|
||||
|
||||
@ -93,13 +93,17 @@ export default Component.extend({
|
||||
},
|
||||
|
||||
finishedEditing() {
|
||||
const oldTagName = this.tag.id;
|
||||
this.tag
|
||||
.update({ id: this.newTagName, description: this.newTagDescription })
|
||||
.then((result) => {
|
||||
this.set("editing", false);
|
||||
this.tagInfo.set("description", this.newTagDescription);
|
||||
if (result.payload) {
|
||||
this.router.transitionTo("tag.show", result.payload.id);
|
||||
if (
|
||||
result.responseJson.tag &&
|
||||
oldTagName !== result.responseJson.tag.id
|
||||
) {
|
||||
this.router.transitionTo("tag.show", result.responseJson.tag.id);
|
||||
}
|
||||
})
|
||||
.catch(popupAjaxError);
|
||||
|
||||
@ -4,6 +4,8 @@ import discourseComputed from "discourse-common/utils/decorators";
|
||||
import { iconHTML } from "discourse-common/lib/icon-library";
|
||||
|
||||
export default Component.extend({
|
||||
disableActions: false,
|
||||
|
||||
classNames: ["topic-statuses"],
|
||||
|
||||
click(e) {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import Component from "@ember/component";
|
||||
import { alias, not } from "@ember/object/computed";
|
||||
import { alias } from "@ember/object/computed";
|
||||
import I18n from "I18n";
|
||||
import UppyUploadMixin from "discourse/mixins/uppy-upload";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
@ -12,15 +12,11 @@ export default Component.extend(UppyUploadMixin, {
|
||||
uploadRootPath: "/admin/backups",
|
||||
uploadUrl: "/admin/backups/upload",
|
||||
|
||||
// TODO (martin) Add functionality to make this usable _without_ multipart
|
||||
// uploads, direct to S3, which needs to call get-presigned-put on the
|
||||
// BackupsController (which extends ExternalUploadHelpers) rather than
|
||||
// the old create_upload_url route. The two are functionally equivalent;
|
||||
// they both generate a presigned PUT url for the upload to S3, and do
|
||||
// the whole thing in one request rather than multipart.
|
||||
|
||||
// direct s3 backups
|
||||
useMultipartUploadsIfAvailable: not("localBackupStorage"),
|
||||
@discourseComputed("localBackupStorage")
|
||||
useMultipartUploadsIfAvailable(localBackupStorage) {
|
||||
return !localBackupStorage && this.siteSettings.enable_direct_s3_uploads;
|
||||
},
|
||||
|
||||
// local backups
|
||||
useChunkedUploads: alias("localBackupStorage"),
|
||||
|
||||
@ -174,7 +174,7 @@ export default Component.extend(CardContentsBase, CanCheckEmails, CleansUp, {
|
||||
include_post_count_for: this.get("topic.id"),
|
||||
};
|
||||
|
||||
User.findByUsername(username, args)
|
||||
return User.findByUsername(username, args)
|
||||
.then((user) => {
|
||||
if (user.topic_post_count) {
|
||||
this.set(
|
||||
@ -183,6 +183,7 @@ export default Component.extend(CardContentsBase, CanCheckEmails, CleansUp, {
|
||||
);
|
||||
}
|
||||
this.setProperties({ user });
|
||||
return user;
|
||||
})
|
||||
.catch(() => this._close())
|
||||
.finally(() => this.set("loading", null));
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import Controller from "@ember/controller";
|
||||
import { action } from "@ember/object";
|
||||
import { empty, notEmpty } from "@ember/object/computed";
|
||||
import { not } from "@ember/object/computed";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import { extractError } from "discourse/lib/ajax-error";
|
||||
import { getNativeContact } from "discourse/lib/pwa-utils";
|
||||
import { emailValid, hostnameValid } from "discourse/lib/utilities";
|
||||
import { bufferedProperty } from "discourse/mixins/buffered-content";
|
||||
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
||||
import Group from "discourse/models/group";
|
||||
@ -28,8 +29,17 @@ export default Controller.extend(
|
||||
inviteToTopic: false,
|
||||
limitToEmail: false,
|
||||
|
||||
isLink: empty("buffered.email"),
|
||||
isEmail: notEmpty("buffered.email"),
|
||||
@discourseComputed("buffered.emailOrDomain")
|
||||
isEmail(emailOrDomain) {
|
||||
return emailValid(emailOrDomain);
|
||||
},
|
||||
|
||||
@discourseComputed("buffered.emailOrDomain")
|
||||
isDomain(emailOrDomain) {
|
||||
return hostnameValid(emailOrDomain);
|
||||
},
|
||||
|
||||
isLink: not("isEmail"),
|
||||
|
||||
onShow() {
|
||||
Group.findAll().then((groups) => {
|
||||
@ -67,6 +77,15 @@ export default Controller.extend(
|
||||
save(opts) {
|
||||
const data = { ...this.buffered.buffer };
|
||||
|
||||
if (data.emailOrDomain) {
|
||||
if (emailValid(data.emailOrDomain)) {
|
||||
data.email = data.emailOrDomain;
|
||||
} else if (hostnameValid(data.emailOrDomain)) {
|
||||
data.domain = data.emailOrDomain;
|
||||
}
|
||||
delete data.emailOrDomain;
|
||||
}
|
||||
|
||||
if (data.groupIds !== undefined) {
|
||||
data.group_ids = data.groupIds.length > 0 ? data.groupIds : "";
|
||||
delete data.groupIds;
|
||||
|
||||
@ -275,6 +275,10 @@ export default Controller.extend(ModalFunctionality, {
|
||||
postAction
|
||||
.act(this.model, params)
|
||||
.then(() => {
|
||||
if (this.isDestroying || this.isDestroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!params.skipClose) {
|
||||
this.send("closeModal");
|
||||
}
|
||||
@ -286,7 +290,9 @@ export default Controller.extend(ModalFunctionality, {
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
this.send("closeModal");
|
||||
if (!this.isDestroying && !this.isDestroyed) {
|
||||
this.send("closeModal");
|
||||
}
|
||||
popupAjaxError(error);
|
||||
});
|
||||
},
|
||||
|
||||
@ -13,7 +13,7 @@ import I18n from "I18n";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { escapeExpression } from "discourse/lib/utilities";
|
||||
import { isEmpty } from "@ember/utils";
|
||||
import { or } from "@ember/object/computed";
|
||||
import { gt, or } from "@ember/object/computed";
|
||||
import { scrollTop } from "discourse/mixins/scroll-top";
|
||||
import { setTransient } from "discourse/lib/page-tracker";
|
||||
import { Promise } from "rsvp";
|
||||
@ -248,6 +248,13 @@ export default Controller.extend({
|
||||
return this.currentUser && this.currentUser.staff && hasResults;
|
||||
},
|
||||
|
||||
hasSelection: gt("selected.length", 0),
|
||||
|
||||
@discourseComputed("selected.length", "model.posts.length")
|
||||
hasUnselectedResults(selectionCount, postsCount) {
|
||||
return selectionCount < postsCount;
|
||||
},
|
||||
|
||||
@discourseComputed("model.grouped_search_result.can_create_topic")
|
||||
canCreateTopic(userCanCreateTopic) {
|
||||
return this.currentUser && userCanCreateTopic;
|
||||
@ -399,18 +406,28 @@ export default Controller.extend({
|
||||
},
|
||||
|
||||
selectAll() {
|
||||
this.selected.addObjects(this.get("model.posts").map((r) => r.topic));
|
||||
this.selected.addObjects(this.get("model.posts")).mapBy("topic");
|
||||
|
||||
// Doing this the proper way is a HUGE pain,
|
||||
// we can hack this to work by observing each on the array
|
||||
// in the component, however, when we select ANYTHING, we would force
|
||||
// 50 traversals of the list
|
||||
// This hack is cheap and easy
|
||||
$(".fps-result input[type=checkbox]").prop("checked", true);
|
||||
document
|
||||
.querySelectorAll(".fps-result input[type=checkbox]")
|
||||
.forEach((checkbox) => {
|
||||
checkbox.checked = true;
|
||||
});
|
||||
},
|
||||
|
||||
clearAll() {
|
||||
this.selected.clear();
|
||||
$(".fps-result input[type=checkbox]").prop("checked", false);
|
||||
|
||||
document
|
||||
.querySelectorAll(".fps-result input[type=checkbox]")
|
||||
.forEach((checkbox) => {
|
||||
checkbox.checked = false;
|
||||
});
|
||||
},
|
||||
|
||||
toggleBulkSelect() {
|
||||
|
||||
@ -45,6 +45,13 @@ export default Controller.extend({
|
||||
inviteExpired: equal("filter", "expired"),
|
||||
invitePending: equal("filter", "pending"),
|
||||
|
||||
@discourseComputed("model")
|
||||
hasEmailInvites(model) {
|
||||
return model.invites.some((invite) => {
|
||||
return invite.email;
|
||||
});
|
||||
},
|
||||
|
||||
@discourseComputed("filter")
|
||||
showBulkActionButtons(filter) {
|
||||
return (
|
||||
@ -57,9 +64,9 @@ export default Controller.extend({
|
||||
canInviteToForum: reads("currentUser.can_invite_to_forum"),
|
||||
canBulkInvite: reads("currentUser.admin"),
|
||||
|
||||
@discourseComputed("invitesCount.total")
|
||||
showSearch(invitesCountTotal) {
|
||||
return invitesCountTotal > 0;
|
||||
@discourseComputed("invitesCount", "filter")
|
||||
showSearch(invitesCount, filter) {
|
||||
return invitesCount[filter] > 5;
|
||||
},
|
||||
|
||||
@action
|
||||
|
||||
@ -13,15 +13,12 @@ export default {
|
||||
return;
|
||||
} // must be logged in
|
||||
|
||||
this.notifications =
|
||||
user.unread_notifications + user.unread_high_priority_notifications;
|
||||
const appEvents = container.lookup("service:app-events");
|
||||
appEvents.on("notifications:changed", () => {
|
||||
const notifications =
|
||||
user.unread_notifications + user.unread_high_priority_notifications;
|
||||
|
||||
container
|
||||
.lookup("service:app-events")
|
||||
.on("notifications:changed", this, "_updateBadge");
|
||||
},
|
||||
|
||||
_updateBadge() {
|
||||
navigator.setAppBadge(this.notifications);
|
||||
navigator.setAppBadge(notifications);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@ -682,6 +682,10 @@ export default {
|
||||
if ($article.length > 0) {
|
||||
$articles.removeClass("selected");
|
||||
$article.addClass("selected");
|
||||
this.appEvents.trigger("keyboard:move-selection", {
|
||||
articles: $articles.get(),
|
||||
selectedArticle: $article.get(0),
|
||||
});
|
||||
|
||||
const articleRect = $article[0].getBoundingClientRect();
|
||||
if (!fast && direction < 0 && articleRect.height > window.innerHeight) {
|
||||
|
||||
@ -120,7 +120,7 @@ export function validateUploadedFile(file, opts) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const IMAGES_EXTENSIONS_REGEX = /(png|jpe?g|gif|svg|ico|heic|heif|webp)/i;
|
||||
export const IMAGES_EXTENSIONS_REGEX = /(png|jpe?g|gif|svg|ico|heic|heif|webp)/i;
|
||||
|
||||
function extensionsToArray(exts) {
|
||||
return exts
|
||||
|
||||
@ -134,6 +134,7 @@ export function highlightPost(postNumber) {
|
||||
element.removeEventListener("animationend", removeHighlighted);
|
||||
};
|
||||
element.addEventListener("animationend", removeHighlighted);
|
||||
container.querySelector(".tabLoc").focus();
|
||||
}
|
||||
|
||||
export function emailValid(email) {
|
||||
@ -142,6 +143,12 @@ export function emailValid(email) {
|
||||
return re.test(email);
|
||||
}
|
||||
|
||||
export function hostnameValid(hostname) {
|
||||
// see: https://stackoverflow.com/questions/106179/regular-expression-to-match-dns-hostname-or-ip-address
|
||||
const re = /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)+([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/;
|
||||
return hostname && re.test(hostname);
|
||||
}
|
||||
|
||||
export function extractDomainFromUrl(url) {
|
||||
if (url.indexOf("://") > -1) {
|
||||
url = url.split("/")[2];
|
||||
|
||||
@ -86,7 +86,9 @@ export default Mixin.create({
|
||||
});
|
||||
|
||||
this.appEvents.trigger("user-card:show", { username });
|
||||
this._showCallback(username, $(target));
|
||||
this._showCallback(username, $(target)).then((user) => {
|
||||
this.appEvents.trigger("user-card:after-show", { user });
|
||||
});
|
||||
|
||||
// We bind scrolling on mobile after cards are shown to hide them if user scrolls
|
||||
if (this.site.mobileView) {
|
||||
|
||||
@ -205,8 +205,6 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, {
|
||||
this._useXHRUploads();
|
||||
}
|
||||
|
||||
// TODO (martin) develop upload handler guidance and an API to use; will
|
||||
// likely be using uppy plugins for this
|
||||
this._uppyInstance.on("file-added", (file) => {
|
||||
if (isPrivateMessage) {
|
||||
file.meta.for_private_message = true;
|
||||
@ -273,6 +271,7 @@ export default Mixin.create(ExtendableUploader, UppyS3Multipart, {
|
||||
fileName: file.name,
|
||||
id: file.id,
|
||||
progress: 0,
|
||||
extension: file.extension,
|
||||
})
|
||||
);
|
||||
const placeholder = this._uploadPlaceholder(file);
|
||||
|
||||
@ -11,10 +11,8 @@ import {
|
||||
} from "discourse/lib/utilities";
|
||||
import { next, schedule } from "@ember/runloop";
|
||||
|
||||
const isInside = (text, regex) => {
|
||||
const matches = text.match(regex);
|
||||
return matches && matches.length % 2;
|
||||
};
|
||||
const INDENT_DIRECTION_LEFT = "left";
|
||||
const INDENT_DIRECTION_RIGHT = "right";
|
||||
|
||||
export default Mixin.create({
|
||||
init() {
|
||||
@ -134,7 +132,10 @@ export default Mixin.create({
|
||||
this.set("value", val.replace(oldVal, newVal));
|
||||
}
|
||||
|
||||
if (opts.forceFocus || this._$textarea.is(":focus")) {
|
||||
if (
|
||||
(opts.forceFocus || this._$textarea.is(":focus")) &&
|
||||
!opts.skipNewSelection
|
||||
) {
|
||||
// Restore cursor.
|
||||
this._selectText(
|
||||
newSelection.start,
|
||||
@ -234,6 +235,11 @@ export default Mixin.create({
|
||||
return null;
|
||||
},
|
||||
|
||||
_isInside(text, regex) {
|
||||
const matches = text.match(regex);
|
||||
return matches && matches.length % 2;
|
||||
},
|
||||
|
||||
@bind
|
||||
paste(e) {
|
||||
if (!this._$textarea.is(":focus") && !isTesting()) {
|
||||
@ -253,7 +259,7 @@ export default Mixin.create({
|
||||
const selected = this._getSelected(null, { lineVal: true });
|
||||
const { pre, value: selectedValue, lineVal } = selected;
|
||||
const isInlinePasting = pre.match(/[^\n]$/);
|
||||
const isCodeBlock = isInside(pre, /(^|\n)```/g);
|
||||
const isCodeBlock = this._isInside(pre, /(^|\n)```/g);
|
||||
|
||||
if (
|
||||
plainText &&
|
||||
@ -273,7 +279,7 @@ export default Mixin.create({
|
||||
if (isInlinePasting) {
|
||||
canPasteHtml = !(
|
||||
lineVal.match(/^```/) ||
|
||||
isInside(pre, /`/g) ||
|
||||
this._isInside(pre, /`/g) ||
|
||||
lineVal.match(/^ /)
|
||||
);
|
||||
} else {
|
||||
@ -285,7 +291,11 @@ export default Mixin.create({
|
||||
this._cachedLinkify &&
|
||||
plainText &&
|
||||
!handled &&
|
||||
selected.end > selected.start
|
||||
selected.end > selected.start &&
|
||||
// text selection does not contain url
|
||||
!this._cachedLinkify.test(selectedValue) &&
|
||||
// text selection does not contain a bbcode-like tag
|
||||
!selectedValue.match(/\[\/?[a-z =]+?\]/g)
|
||||
) {
|
||||
if (this._cachedLinkify.test(plainText)) {
|
||||
const match = this._cachedLinkify.match(plainText)[0];
|
||||
@ -311,8 +321,10 @@ export default Mixin.create({
|
||||
markdown = pre.match(/\S$/) ? ` ${markdown}` : markdown;
|
||||
}
|
||||
|
||||
this.appEvents.trigger("composer:insert-text", markdown);
|
||||
handled = true;
|
||||
if (isComposer) {
|
||||
this.appEvents.trigger("composer:insert-text", markdown);
|
||||
handled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -321,6 +333,94 @@ export default Mixin.create({
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Removes the provided char from the provided str up
|
||||
* until the limit, or until a character that is _not_
|
||||
* the provided one is encountered.
|
||||
*/
|
||||
_deindentLine(str, char, limit) {
|
||||
let eaten = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
if (eaten < limit && str[i] === char) {
|
||||
eaten += 1;
|
||||
} else {
|
||||
return str.slice(eaten);
|
||||
}
|
||||
}
|
||||
return str;
|
||||
},
|
||||
|
||||
@bind
|
||||
_indentSelection(direction) {
|
||||
if (![INDENT_DIRECTION_LEFT, INDENT_DIRECTION_RIGHT].includes(direction)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selected = this._getSelected(null, { lineVal: true });
|
||||
const { lineVal } = selected;
|
||||
let value = selected.value;
|
||||
|
||||
// Perhaps this is a bit simplistic, but it is a fairly reliable
|
||||
// guess to say whether we are indenting with tabs or spaces. for
|
||||
// example some programming languages prefer tabs, others prefer
|
||||
// spaces, and for the cases with no tabs it's safer to use spaces
|
||||
let indentationSteps, indentationChar;
|
||||
let linesStartingWithTabCount = value.match(/^\t/gm)?.length || 0;
|
||||
let linesStartingWithSpaceCount = value.match(/^ /gm)?.length || 0;
|
||||
if (linesStartingWithTabCount > linesStartingWithSpaceCount) {
|
||||
indentationSteps = 1;
|
||||
indentationChar = "\t";
|
||||
} else {
|
||||
indentationChar = " ";
|
||||
indentationSteps = 2;
|
||||
}
|
||||
|
||||
// We want to include all the spaces on the selected line as
|
||||
// well, no matter where the cursor begins on the first line,
|
||||
// because we want to indent those too. * is the cursor/selection
|
||||
// and . are spaces:
|
||||
//
|
||||
// BEFORE AFTER
|
||||
//
|
||||
// * *
|
||||
// ....text here ....text here
|
||||
// ....some more text ....some more text
|
||||
// * *
|
||||
//
|
||||
// BEFORE AFTER
|
||||
//
|
||||
// * *
|
||||
// ....text here ....text here
|
||||
// ....some more text ....some more text
|
||||
// * *
|
||||
const indentationRegexp = new RegExp(`^${indentationChar}+`);
|
||||
const lineStartsWithIndentationChar = lineVal.match(indentationRegexp);
|
||||
const intentationCharsBeforeSelection = value.match(indentationRegexp);
|
||||
if (lineStartsWithIndentationChar) {
|
||||
const charsToSubtract = intentationCharsBeforeSelection
|
||||
? intentationCharsBeforeSelection[0]
|
||||
: "";
|
||||
value =
|
||||
lineStartsWithIndentationChar[0].replace(charsToSubtract, "") + value;
|
||||
}
|
||||
|
||||
const splitSelection = value.split("\n");
|
||||
const newValue = splitSelection
|
||||
.map((line) => {
|
||||
if (direction === INDENT_DIRECTION_LEFT) {
|
||||
return this._deindentLine(line, indentationChar, indentationSteps);
|
||||
} else {
|
||||
return `${Array(indentationSteps + 1).join(indentationChar)}${line}`;
|
||||
}
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
if (newValue.trim() !== "") {
|
||||
this._replaceText(value, newValue, { skipNewSelection: true });
|
||||
this._selectText(this.value.indexOf(newValue), newValue.length);
|
||||
}
|
||||
},
|
||||
|
||||
@action
|
||||
emojiSelected(code) {
|
||||
let selected = this._getSelected();
|
||||
|
||||
@ -0,0 +1,17 @@
|
||||
import EmberObject from "@ember/object";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
|
||||
const AssociatedGroup = EmberObject.extend();
|
||||
|
||||
AssociatedGroup.reopenClass({
|
||||
list() {
|
||||
return ajax("/associated_groups")
|
||||
.then((result) => {
|
||||
return result.associated_groups.map((ag) => AssociatedGroup.create(ag));
|
||||
})
|
||||
.catch(popupAjaxError);
|
||||
},
|
||||
});
|
||||
|
||||
export default AssociatedGroup;
|
||||
@ -29,6 +29,11 @@ const Group = RestModel.extend({
|
||||
return isEmpty(value) ? "" : value;
|
||||
},
|
||||
|
||||
@discourseComputed("associated_group_ids")
|
||||
associatedGroupIds(value) {
|
||||
return isEmpty(value) ? [] : value;
|
||||
},
|
||||
|
||||
@discourseComputed("automatic")
|
||||
type(automatic) {
|
||||
return automatic ? "automatic" : "custom";
|
||||
@ -277,6 +282,11 @@ const Group = RestModel.extend({
|
||||
}
|
||||
);
|
||||
|
||||
let agIds = this.associated_group_ids;
|
||||
if (agIds) {
|
||||
attrs["associated_group_ids"] = agIds.length ? agIds : [null];
|
||||
}
|
||||
|
||||
if (this.flair_type === "icon") {
|
||||
attrs["flair_icon"] = this.flair_icon;
|
||||
} else if (this.flair_type === "image") {
|
||||
|
||||
@ -49,6 +49,11 @@ const Invite = EmberObject.extend({
|
||||
return topicData ? Topic.create(topicData) : null;
|
||||
},
|
||||
|
||||
@discourseComputed("email", "domain")
|
||||
emailOrDomain(email, domain) {
|
||||
return email || domain;
|
||||
},
|
||||
|
||||
topicId: alias("topics.firstObject.id"),
|
||||
topicTitle: alias("topics.firstObject.title"),
|
||||
});
|
||||
|
||||
@ -266,11 +266,13 @@ export default RestModel.extend({
|
||||
filterReplies(postNumber, postId) {
|
||||
this.cancelFilter();
|
||||
this.set("filterRepliesToPostNumber", postNumber);
|
||||
|
||||
this.appEvents.trigger("post-stream:filter-replies", {
|
||||
topic_id: this.get("topic.id"),
|
||||
post_number: postNumber,
|
||||
post_id: postId,
|
||||
});
|
||||
|
||||
return this.refresh({ refreshInPlace: true }).then(() => {
|
||||
const element = document.querySelector(`#post_${postNumber}`);
|
||||
|
||||
@ -280,16 +282,13 @@ export default RestModel.extend({
|
||||
: null;
|
||||
|
||||
this.appEvents.trigger("post-stream:refresh");
|
||||
|
||||
DiscourseURL.jumpToPost(postNumber, {
|
||||
originalTopOffset,
|
||||
});
|
||||
|
||||
const replyPostNumbers = this.posts.mapBy("post_number");
|
||||
replyPostNumbers.splice(0, 2);
|
||||
schedule("afterRender", () => {
|
||||
replyPostNumbers.forEach((postNum) => {
|
||||
highlightPost(postNum);
|
||||
});
|
||||
highlightPost(postNumber);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
@ -41,6 +41,10 @@ export default {
|
||||
caps.hasContactPicker =
|
||||
"contacts" in navigator && "ContactsManager" in window;
|
||||
caps.canVibrate = "vibrate" in navigator;
|
||||
caps.isPwa =
|
||||
window.matchMedia("(display-mode: standalone)").matches ||
|
||||
window.navigator.standalone ||
|
||||
document.referrer.includes("android-app://");
|
||||
|
||||
// Inject it
|
||||
app.register("capabilities:main", caps, { instantiate: false });
|
||||
|
||||
@ -19,6 +19,7 @@ import userPresent, {
|
||||
onPresenceChange,
|
||||
removeOnPresenceChange,
|
||||
} from "discourse/lib/user-presence";
|
||||
import { bind } from "discourse-common/utils/decorators";
|
||||
|
||||
const PRESENCE_INTERVAL_S = 30;
|
||||
const PRESENCE_DEBOUNCE_MS = isTesting() ? 0 : 500;
|
||||
@ -26,7 +27,7 @@ const PRESENCE_THROTTLE_MS = isTesting() ? 0 : 1000;
|
||||
|
||||
const PRESENCE_GET_RETRY_MS = 5000;
|
||||
|
||||
const USER_PRESENCE_ARGS = {
|
||||
const DEFAULT_ACTIVE_OPTIONS = {
|
||||
userUnseenTime: 60000,
|
||||
browserHiddenTime: 10000,
|
||||
};
|
||||
@ -63,8 +64,19 @@ class PresenceChannel extends EmberObject {
|
||||
// By default, the user will temporarily 'leave' the channel when
|
||||
// the current tab is in the background, or has no interaction for more than 60 seconds.
|
||||
// To override this behaviour, set onlyWhileActive: false
|
||||
async enter({ onlyWhileActive = true } = {}) {
|
||||
this.setProperties({ onlyWhileActive });
|
||||
// To specify custom thresholds, set `activeOptions`. See `lib/user-presence.js` for options.
|
||||
async enter({ onlyWhileActive = true, activeOptions = null } = {}) {
|
||||
if (onlyWhileActive && activeOptions) {
|
||||
for (const key in DEFAULT_ACTIVE_OPTIONS) {
|
||||
if (activeOptions[key] < DEFAULT_ACTIVE_OPTIONS[key]) {
|
||||
throw `${key} cannot be less than ${DEFAULT_ACTIVE_OPTIONS[key]} (given ${activeOptions[key]})`;
|
||||
}
|
||||
}
|
||||
} else if (onlyWhileActive && !activeOptions) {
|
||||
activeOptions = DEFAULT_ACTIVE_OPTIONS;
|
||||
}
|
||||
|
||||
this.setProperties({ activeOptions });
|
||||
await this.presenceService._enter(this);
|
||||
this.set("present", true);
|
||||
}
|
||||
@ -241,13 +253,10 @@ export default class PresenceService extends Service {
|
||||
this._initialDataRequests = new Map();
|
||||
|
||||
if (this.currentUser) {
|
||||
this._beforeUnloadCallback = () => this._beaconLeaveAll();
|
||||
window.addEventListener("beforeunload", this._beforeUnloadCallback);
|
||||
|
||||
this._presenceChangeCallback = () => this._throttledUpdateServer();
|
||||
window.addEventListener("beforeunload", this._beaconLeaveAll);
|
||||
onPresenceChange({
|
||||
...USER_PRESENCE_ARGS,
|
||||
callback: this._presenceChangeCallback,
|
||||
...DEFAULT_ACTIVE_OPTIONS,
|
||||
callback: this._throttledUpdateServer,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -258,8 +267,8 @@ export default class PresenceService extends Service {
|
||||
|
||||
willDestroy() {
|
||||
super.willDestroy(...arguments);
|
||||
window.removeEventListener("beforeunload", this._beforeUnloadCallback);
|
||||
removeOnPresenceChange(this._presenceChangeCallback);
|
||||
window.removeEventListener("beforeunload", this._beaconLeaveAll);
|
||||
removeOnPresenceChange(this._throttledUpdateServer);
|
||||
}
|
||||
|
||||
// Get a PresenceChannel object representing a single channel
|
||||
@ -440,6 +449,7 @@ export default class PresenceService extends Service {
|
||||
}
|
||||
}
|
||||
|
||||
@bind
|
||||
_beaconLeaveAll() {
|
||||
if (isTesting()) {
|
||||
return;
|
||||
@ -490,15 +500,15 @@ export default class PresenceService extends Service {
|
||||
.filter((e) => e.type === "leave")
|
||||
.map((e) => e.channel);
|
||||
|
||||
const userIsPresent = userPresent(USER_PRESENCE_ARGS);
|
||||
for (const [channelName, proxies] of this._presentProxies) {
|
||||
if (
|
||||
!userIsPresent &&
|
||||
Array.from(proxies).every((p) => p.onlyWhileActive)
|
||||
Array.from(proxies).some((p) => {
|
||||
return !p.activeOptions || userPresent(p.activeOptions);
|
||||
})
|
||||
) {
|
||||
channelsToLeave.push(channelName);
|
||||
} else {
|
||||
presentChannels.push(channelName);
|
||||
} else {
|
||||
channelsToLeave.push(channelName);
|
||||
}
|
||||
}
|
||||
|
||||
@ -551,6 +561,7 @@ export default class PresenceService extends Service {
|
||||
// in a sequence of calls. We want both. We want the first event, to make
|
||||
// things very responsive. Then if things are happening too frequently, we
|
||||
// drop back to the last event via the regular throttle function.
|
||||
@bind
|
||||
_throttledUpdateServer() {
|
||||
if (
|
||||
!this._lastUpdate ||
|
||||
|
||||
@ -1,8 +1,15 @@
|
||||
<label for="choose-topic-title">
|
||||
{{#if labelIcon}}{{d-icon labelIcon}}{{/if}}{{i18n labelText}}
|
||||
{{#if labelIcon}}
|
||||
{{d-icon labelIcon}}
|
||||
{{/if}}
|
||||
<span>{{i18n labelText}}</span>
|
||||
</label>
|
||||
|
||||
{{text-field value=topicTitle placeholderKey="choose_topic.title.placeholder" id="choose-topic-title"}}
|
||||
{{text-field
|
||||
value=topicTitle
|
||||
placeholderKey="choose_topic.title.placeholder"
|
||||
id="choose-topic-title"
|
||||
}}
|
||||
|
||||
{{#if loading}}
|
||||
<p>{{i18n "loading"}}</p>
|
||||
@ -13,13 +20,23 @@
|
||||
{{#each topics as |t|}}
|
||||
<div class="controls existing-topic">
|
||||
<label class="radio">
|
||||
<input id="choose-topic-{{t.id}}" {{action "chooseTopic" t}} type="radio" name="choose_topic_id">
|
||||
{{raw "topic-status" topic=t}}
|
||||
{{input
|
||||
id=(concat "choose-topic-" t.id)
|
||||
checked=(eq t.id selectedTopicId)
|
||||
click=(action "chooseTopic" t)
|
||||
type="radio"
|
||||
name="choose_topic_id"
|
||||
}}
|
||||
{{topic-status topic=t disableActions=true}}
|
||||
<span class="topic-title">
|
||||
{{replace-emoji t.fancy_title}}
|
||||
</span>
|
||||
<span class="topic-categories">
|
||||
{{bound-category-link t.category recursive=true hideParent=true}}
|
||||
{{bound-category-link t.category
|
||||
recursive=true
|
||||
hideParent=true
|
||||
link=false
|
||||
}}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@ -10,6 +10,9 @@
|
||||
post=model.post
|
||||
whisper=model.whisper
|
||||
noBump=model.noBump
|
||||
options=(hash
|
||||
mobilePlacementStrategy="fixed"
|
||||
)
|
||||
}}
|
||||
|
||||
<span class="action-title">
|
||||
|
||||
@ -31,7 +31,7 @@
|
||||
|
||||
{{#if tag}}
|
||||
{{#if showToggleInfo}}
|
||||
{{d-button icon="wrench" class="btn-default" ariaLabel="tagging.info" action=toggleInfo id="show-tag-info"}}
|
||||
{{d-button icon=(if currentUser.staff "wrench" "info-circle") class="btn-default" ariaLabel="tagging.info" action=toggleInfo id="show-tag-info"}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
|
||||
@ -59,6 +59,23 @@
|
||||
onChange=(action "onChangeEmailDomainsSetting")
|
||||
options=(hash allowAny=true)
|
||||
}}
|
||||
|
||||
{{#if showAssociatedGroups}}
|
||||
<label for="automatic_membership_associated_groups">
|
||||
{{i18n "admin.groups.manage.membership.automatic_membership_associated_groups"}}
|
||||
</label>
|
||||
|
||||
{{list-setting
|
||||
name="automatic_membership_associated_groups"
|
||||
class="group-form-automatic-membership-associated-groups"
|
||||
value=model.associatedGroupIds
|
||||
choices=associatedGroups
|
||||
settingName="name"
|
||||
nameProperty="label"
|
||||
valueProperty="id"
|
||||
onChange=(action (mut model.associated_group_ids))
|
||||
}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{plugin-outlet name="groups-form-membership-below-automatic"
|
||||
|
||||
@ -17,24 +17,11 @@
|
||||
<div class="tag-name-wrapper">
|
||||
{{discourse-tag tagInfo.name tagName="div" size="large"}}
|
||||
{{#if canAdminTag}}
|
||||
<a href {{action "edit"}} id="edit-tag" title={{i18n "tagging.edit_tag"}}>{{d-icon "pencil-alt"}}</a>
|
||||
{{d-button action=(action "edit") class="btn-flat edit-tag" title="tagging.edit_tag" icon="pencil-alt" }}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{#if canAdminTag}}
|
||||
<div class="tag-description-wrapper">
|
||||
{{tagInfo.description}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
{{#if canAdminTag}}
|
||||
{{plugin-outlet name="tag-custom-settings" args=(hash tag=tagInfo) connectorTagName="" tagName="section"}}
|
||||
|
||||
<div class="tag-actions">
|
||||
{{d-button class="btn-default" action=(action "toggleEditControls") icon="cog" label="tagging.edit_synonyms" id="edit-synonyms"}}
|
||||
{{#if deleteAction}}
|
||||
{{d-button class="btn-danger delete-tag" action=(action "deleteTag") icon="far-trash-alt" label="tagging.delete_tag" id="delete-tag"}}
|
||||
{{/if}}
|
||||
<div class="tag-description-wrapper">
|
||||
{{tagInfo.description}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
@ -53,7 +40,10 @@
|
||||
{{#if tagInfo.category_restricted}}
|
||||
{{i18n "tagging.category_restricted"}}
|
||||
{{else}}
|
||||
{{html-safe (i18n "tagging.default_info" basePath=(base-path))}}
|
||||
{{html-safe (i18n "tagging.default_info")}}
|
||||
{{#if canAdminTag}}
|
||||
{{html-safe (i18n "tagging.staff_info" basePath=(base-path))}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
@ -81,20 +71,31 @@
|
||||
{{#if editSynonymsMode}}
|
||||
<section class="add-synonyms field">
|
||||
<label for="add-synonyms">{{i18n "tagging.add_synonyms_label"}}</label>
|
||||
{{tag-chooser
|
||||
id="add-synonyms"
|
||||
tags=newSynonyms
|
||||
blockedTags=(array tagInfo.name)
|
||||
everyTag=true
|
||||
excludeSynonyms=true
|
||||
excludeHasSynonyms=true
|
||||
unlimitedTagCount=true}}
|
||||
<div>
|
||||
{{tag-chooser
|
||||
id="add-synonyms"
|
||||
tags=newSynonyms
|
||||
blockedTags=(array tagInfo.name)
|
||||
everyTag=true
|
||||
excludeSynonyms=true
|
||||
excludeHasSynonyms=true
|
||||
unlimitedTagCount=true}}
|
||||
{{d-button
|
||||
class="ok"
|
||||
action=(action "addSynonyms")
|
||||
disabled=addSynonymsDisabled
|
||||
icon="check"}}
|
||||
</div>
|
||||
</section>
|
||||
{{d-button
|
||||
class="btn-default"
|
||||
action=(action "addSynonyms")
|
||||
disabled=addSynonymsDisabled
|
||||
label="tagging.add_synonyms"}}
|
||||
{{/if}}
|
||||
{{#if canAdminTag}}
|
||||
{{plugin-outlet name="tag-custom-settings" args=(hash tag=tagInfo) connectorTagName="" tagName="section"}}
|
||||
<div class="tag-actions">
|
||||
{{d-button class="btn-default" action=(action "toggleEditControls") icon="cog" label="tagging.edit_synonyms" id="edit-synonyms"}}
|
||||
{{#if deleteAction}}
|
||||
{{d-button class="btn-danger delete-tag" action=(action "deleteTag") icon="far-trash-alt" label="tagging.delete_tag" id="delete-tag"}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{#if loading}}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{{#if this.field.name}}
|
||||
<label class="control-label" for={{concat "user-" this.elementId}}>
|
||||
<label class="control-label">
|
||||
{{html-safe this.field.name}} {{#if this.field.required}}<span class="required">*</span>{{/if}}
|
||||
</label>
|
||||
{{/if}}
|
||||
|
||||
@ -44,6 +44,9 @@
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/unless}}
|
||||
|
||||
{{plugin-outlet name="before-composer-controls" args=(hash model=model) tagName="" connectorTagName=""}}
|
||||
|
||||
{{composer-toggles
|
||||
composeState=model.composeState
|
||||
showToolbar=showToolbar
|
||||
|
||||
@ -0,0 +1 @@
|
||||
<section>{{yield}}</section>
|
||||
@ -81,8 +81,23 @@
|
||||
{{/if}}
|
||||
|
||||
{{#if bulkSelectEnabled}}
|
||||
{{d-button icon="check-square" class="btn-default" action=(action "selectAll") label="search.select_all"~}}
|
||||
{{d-button icon="far-square" class="btn-default" action=(action "clearAll") label="search.clear_all"}}
|
||||
{{#if hasUnselectedResults}}
|
||||
{{d-button
|
||||
icon="check-square"
|
||||
class="btn-default"
|
||||
action=(action "selectAll")
|
||||
label="search.select_all"
|
||||
}}
|
||||
{{/if}}
|
||||
|
||||
{{#if hasSelection}}
|
||||
{{d-button
|
||||
icon="far-square"
|
||||
class="btn-default"
|
||||
action=(action "clearAll")
|
||||
label="search.clear_all"
|
||||
}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
<div class="sort-by inline-form">
|
||||
|
||||
@ -56,6 +56,7 @@
|
||||
{{#if canLoginLocal}}
|
||||
{{#unless showSecurityKey }}
|
||||
{{d-button action=(action "login")
|
||||
id="login-button"
|
||||
icon="unlock"
|
||||
label=loginButtonLabel
|
||||
disabled=loginDisabled
|
||||
|
||||
@ -37,12 +37,21 @@
|
||||
{{/if}}
|
||||
|
||||
<div class="input-group input-email">
|
||||
<label for="invite-email">{{d-icon "envelope"}}{{i18n "user.invited.invite.restrict_email"}}</label>
|
||||
<label for="invite-email">
|
||||
{{d-icon "envelope"}}
|
||||
{{#if isEmail}}
|
||||
{{i18n "user.invited.invite.restrict_email"}}
|
||||
{{else if isDomain}}
|
||||
{{i18n "user.invited.invite.restrict_domain"}}
|
||||
{{else}}
|
||||
{{i18n "user.invited.invite.restrict"}}
|
||||
{{/if}}
|
||||
</label>
|
||||
<div class="invite-email-container">
|
||||
{{input
|
||||
id="invite-email"
|
||||
value=buffered.email
|
||||
placeholderKey="topic.invite_reply.email_placeholder"
|
||||
value=buffered.emailOrDomain
|
||||
placeholderKey="user.invited.invite.email_or_domain_placeholder"
|
||||
}}
|
||||
{{#if capabilities.hasContactPicker}}
|
||||
{{d-button
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
{{#d-section bodyClass="navigation-topics" class="navigation-container" scrollTop="false"}}
|
||||
{{#d-section bodyClass="navigation-topics" class="navigation-container" scrollTop=false}}
|
||||
{{d-navigation
|
||||
filterMode=filterMode
|
||||
canCreateTopic=canCreateTopic
|
||||
|
||||
@ -17,9 +17,9 @@
|
||||
{{#if showBulkActionButtons}}
|
||||
{{#if inviteExpired}}
|
||||
{{#if removedAll}}
|
||||
<li>
|
||||
<span class="removed-all">
|
||||
{{i18n "user.invited.removed_all"}}
|
||||
</li>
|
||||
</span>
|
||||
{{else}}
|
||||
{{d-button icon="times" action=(action "destroyAllExpired") label="user.invited.remove_all"}}
|
||||
{{/if}}
|
||||
@ -27,10 +27,10 @@
|
||||
|
||||
{{#if invitePending}}
|
||||
{{#if reinvitedAll}}
|
||||
<li>
|
||||
{{i18n "user.invited.reinvited_all"}}
|
||||
</li>
|
||||
{{else}}
|
||||
<span class="reinvited-all">
|
||||
{{d-button icon="check" disabled=true label="user.invited.reinvited_all"}}
|
||||
</span>
|
||||
{{else if hasEmailInvites}}
|
||||
{{d-button class="btn-default" icon="sync" action=(action "reinviteAll") label="user.invited.reinvite_all"}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
{{#d-section pageClass="user-activity" class="user-secondary-navigation" scrollTop="false"}}
|
||||
{{#d-section pageClass="user-activity" class="user-secondary-navigation" scrollTop=false}}
|
||||
<nav role="navigation">
|
||||
{{#mobile-nav class="activity-nav" desktopClass="action-list activity-list nav-stacked"}}
|
||||
{{#d-navigation-item route="userActivity.index"}}{{i18n "user.filters.all"}}{{/d-navigation-item}}
|
||||
|
||||
@ -186,18 +186,19 @@ createWidget("post-avatar", {
|
||||
});
|
||||
}
|
||||
|
||||
const result = [body];
|
||||
const postAvatarBody = [body];
|
||||
|
||||
if (attrs.flair_url || attrs.flair_bg_color) {
|
||||
result.push(this.attach("avatar-flair", attrs));
|
||||
postAvatarBody.push(this.attach("avatar-flair", attrs));
|
||||
} else {
|
||||
const autoFlairAttrs = autoGroupFlairForUser(this.site, attrs);
|
||||
|
||||
if (autoFlairAttrs) {
|
||||
result.push(this.attach("avatar-flair", autoFlairAttrs));
|
||||
postAvatarBody.push(this.attach("avatar-flair", autoFlairAttrs));
|
||||
}
|
||||
}
|
||||
|
||||
result.push(h("div.poster-avatar-extra"));
|
||||
const result = [h("div.post-avatar", postAvatarBody)];
|
||||
|
||||
if (this.settings.displayPosterName) {
|
||||
result.push(this.attach("post-avatar-user-info", attrs));
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import RawHtml from "discourse/widgets/raw-html";
|
||||
import { iconHTML } from "discourse-common/lib/icon-library";
|
||||
import QuickAccessPanel from "discourse/widgets/quick-access-panel";
|
||||
import UserAction from "discourse/models/user-action";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { createWidget, createWidgetFrom } from "discourse/widgets/widget";
|
||||
import { h } from "virtual-dom";
|
||||
@ -72,14 +71,4 @@ createWidgetFrom(QuickAccessPanel, "quick-access-bookmarks", {
|
||||
({ user_bookmark_list }) => user_bookmark_list.bookmarks
|
||||
);
|
||||
},
|
||||
|
||||
loadUserActivityBookmarks() {
|
||||
return ajax("/user_actions.json", {
|
||||
data: {
|
||||
username: this.currentUser.username,
|
||||
filter: UserAction.TYPES.bookmarks,
|
||||
no_results_help_key: "user_activity.no_bookmarks",
|
||||
},
|
||||
}).then(({ user_actions }) => user_actions);
|
||||
},
|
||||
});
|
||||
|
||||
@ -25,6 +25,9 @@ module.exports = function (defaults) {
|
||||
// This forces the use of `fast-sourcemap-concat` which works in production.
|
||||
enabled: true,
|
||||
},
|
||||
autoImport: {
|
||||
forbidEval: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Ember CLI does this by default for the app tree, but for our extra bundles we
|
||||
|
||||
@ -6,6 +6,7 @@ const { encode } = require("html-entities");
|
||||
const cleanBaseURL = require("clean-base-url");
|
||||
const path = require("path");
|
||||
const { promises: fs } = require("fs");
|
||||
const { JSDOM } = require("jsdom");
|
||||
|
||||
// via https://stackoverflow.com/a/6248722/165668
|
||||
function generateUID() {
|
||||
@ -168,36 +169,40 @@ function replaceIn(bootstrap, template, id, headers, baseURL) {
|
||||
return template.replace(`<bootstrap-content key="${id}">`, contents);
|
||||
}
|
||||
|
||||
async function applyBootstrap(bootstrap, template, response, baseURL) {
|
||||
// If our initial page added some preload data let's not lose that.
|
||||
let json = await response.json();
|
||||
if (json && json.preloaded) {
|
||||
bootstrap.preloaded = Object.assign(json.preloaded, bootstrap.preloaded);
|
||||
function extractPreloadJson(html) {
|
||||
const dom = new JSDOM(html);
|
||||
const dataElement = dom.window.document.querySelector("#data-preloaded");
|
||||
|
||||
if (!dataElement || !dataElement.dataset) {
|
||||
return;
|
||||
}
|
||||
|
||||
return dataElement.dataset.preloaded;
|
||||
}
|
||||
|
||||
async function applyBootstrap(bootstrap, template, response, baseURL, preload) {
|
||||
bootstrap.preloaded = Object.assign(JSON.parse(preload), bootstrap.preloaded);
|
||||
|
||||
Object.keys(BUILDERS).forEach((id) => {
|
||||
template = replaceIn(bootstrap, template, id, response.headers, baseURL);
|
||||
});
|
||||
return template;
|
||||
}
|
||||
|
||||
async function buildFromBootstrap(proxy, baseURL, req, response) {
|
||||
async function buildFromBootstrap(proxy, baseURL, req, response, preload) {
|
||||
try {
|
||||
const template = await fs.readFile(
|
||||
path.join(process.cwd(), "dist", "index.html"),
|
||||
"utf8"
|
||||
);
|
||||
|
||||
let url = `${proxy}${baseURL}bootstrap.json`;
|
||||
const queryLoc = req.url.indexOf("?");
|
||||
if (queryLoc !== -1) {
|
||||
url += req.url.substr(queryLoc);
|
||||
}
|
||||
let url = new URL(`${proxy}${baseURL}bootstrap.json`);
|
||||
url.searchParams.append("for_url", req.url);
|
||||
|
||||
const res = await fetch(url, { headers: req.headers });
|
||||
const json = await res.json();
|
||||
|
||||
return applyBootstrap(json.bootstrap, template, response, baseURL);
|
||||
return applyBootstrap(json.bootstrap, template, response, baseURL, preload);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Could not get ${proxy}${baseURL}bootstrap.json\n\n${error}`
|
||||
@ -229,13 +234,13 @@ async function handleRequest(proxy, baseURL, req, res) {
|
||||
|
||||
if (req.method === "GET") {
|
||||
req.headers["X-Discourse-Ember-CLI"] = "true";
|
||||
req.headers["X-Discourse-Asset-Path"] = req.path;
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: req.method,
|
||||
body: /GET|HEAD/.test(req.method) ? null : req.body,
|
||||
headers: req.headers,
|
||||
redirect: "manual",
|
||||
});
|
||||
|
||||
response.headers.forEach((value, header) => {
|
||||
@ -251,20 +256,38 @@ async function handleRequest(proxy, baseURL, req, res) {
|
||||
|
||||
const csp = response.headers.get("content-security-policy");
|
||||
if (csp) {
|
||||
const newCSP = csp.replace(
|
||||
new RegExp(proxy, "g"),
|
||||
`http://${originalHost}`
|
||||
);
|
||||
const emberCliAdditions = [
|
||||
`http://${originalHost}/assets/`,
|
||||
`http://${originalHost}/ember-cli-live-reload.js`,
|
||||
`http://${originalHost}/_lr/`,
|
||||
];
|
||||
const newCSP = csp
|
||||
.replace(new RegExp(proxy, "g"), `http://${originalHost}`)
|
||||
.replace(
|
||||
new RegExp("script-src ", "g"),
|
||||
`script-src ${emberCliAdditions.join(" ")} `
|
||||
);
|
||||
res.set("content-security-policy", newCSP);
|
||||
}
|
||||
|
||||
if (response.headers.get("x-discourse-bootstrap-required") === "true") {
|
||||
const html = await buildFromBootstrap(proxy, baseURL, req, response);
|
||||
const contentType = response.headers.get("content-type");
|
||||
const isHTML = contentType && contentType.startsWith("text/html");
|
||||
const responseText = await response.text();
|
||||
const preloadJson = isHTML ? extractPreloadJson(responseText) : null;
|
||||
|
||||
if (preloadJson) {
|
||||
const html = await buildFromBootstrap(
|
||||
proxy,
|
||||
baseURL,
|
||||
req,
|
||||
response,
|
||||
extractPreloadJson(responseText)
|
||||
);
|
||||
res.set("content-type", "text/html");
|
||||
res.send(html);
|
||||
} else {
|
||||
res.status(response.status);
|
||||
res.send(await response.text());
|
||||
res.send(responseText);
|
||||
}
|
||||
}
|
||||
|
||||
@ -301,8 +324,8 @@ to serve API requests. For example:
|
||||
} catch (error) {
|
||||
res.send(`
|
||||
<html>
|
||||
<h1>Discourse Build Error</h1>
|
||||
<pre><code>${error}</code></pre>
|
||||
<h1>Discourse Ember CLI Proxy Error</h1>
|
||||
<pre><code>${error.stack}</code></pre>
|
||||
</html>
|
||||
`);
|
||||
} finally {
|
||||
|
||||
@ -54,6 +54,7 @@
|
||||
"eslint": "^7.27.0",
|
||||
"html-entities": "^2.1.0",
|
||||
"js-yaml": "^4.0.0",
|
||||
"jsdom": "^18.1.1",
|
||||
"loader.js": "^4.7.0",
|
||||
"message-bus-client": "^3.3.0",
|
||||
"messageformat": "0.1.5",
|
||||
|
||||
@ -2,6 +2,7 @@ import {
|
||||
acceptance,
|
||||
count,
|
||||
exists,
|
||||
query,
|
||||
queryAll,
|
||||
updateCurrentUser,
|
||||
} from "discourse/tests/helpers/qunit-helpers";
|
||||
@ -187,7 +188,7 @@ acceptance("Composer Actions", function (needs) {
|
||||
assert.deepEqual(privateMessageUsers.header().value(), "foo,foo_group");
|
||||
});
|
||||
|
||||
test("hide component if no content", async function (assert) {
|
||||
test("allow switching back to New Topic", async function (assert) {
|
||||
await visit("/");
|
||||
await click("button#create-topic");
|
||||
|
||||
@ -195,12 +196,18 @@ acceptance("Composer Actions", function (needs) {
|
||||
await composerActions.expand();
|
||||
await composerActions.selectRowByValue("reply_as_private_message");
|
||||
|
||||
assert.ok(composerActions.el().hasClass("is-hidden"));
|
||||
assert.strictEqual(composerActions.el().children().length, 0);
|
||||
assert.strictEqual(
|
||||
query(".action-title").innerText,
|
||||
I18n.t("topic.private_message")
|
||||
);
|
||||
|
||||
await click("button#create-topic");
|
||||
await composerActions.expand();
|
||||
assert.strictEqual(composerActions.rows().length, 2);
|
||||
await composerActions.selectRowByValue("create_topic");
|
||||
|
||||
assert.strictEqual(
|
||||
query(".action-title").innerText,
|
||||
I18n.t("topic.create_long")
|
||||
);
|
||||
});
|
||||
|
||||
test("interactions", async function (assert) {
|
||||
|
||||
@ -6,7 +6,7 @@ import {
|
||||
} from "discourse/tests/helpers/qunit-helpers";
|
||||
import { click, fillIn, visit } from "@ember/test-helpers";
|
||||
import selectKit from "discourse/tests/helpers/select-kit-helper";
|
||||
import { skip, test } from "qunit";
|
||||
import { test } from "qunit";
|
||||
import userFixtures from "discourse/tests/fixtures/user-fixtures";
|
||||
import { run } from "@ember/runloop";
|
||||
|
||||
@ -153,7 +153,7 @@ acceptance("flagging", function (needs) {
|
||||
assert.ok(!exists(".bootbox.modal:visible"));
|
||||
});
|
||||
|
||||
skip("CTRL + ENTER accepts the modal", async function (assert) {
|
||||
test("CTRL + ENTER accepts the modal", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
await openFlagModal();
|
||||
|
||||
@ -169,7 +169,7 @@ acceptance("flagging", function (needs) {
|
||||
assert.ok(!exists("#discourse-modal:visible"), "The modal was closed");
|
||||
});
|
||||
|
||||
skip("CMD or WINDOWS-KEY + ENTER accepts the modal", async function (assert) {
|
||||
test("CMD or WINDOWS-KEY + ENTER accepts the modal", async function (assert) {
|
||||
await visit("/t/internationalization-localization/280");
|
||||
await openFlagModal();
|
||||
|
||||
|
||||
@ -7,9 +7,24 @@ import {
|
||||
import { click, visit } from "@ember/test-helpers";
|
||||
import selectKit from "discourse/tests/helpers/select-kit-helper";
|
||||
import { test } from "qunit";
|
||||
import Site from "discourse/models/site";
|
||||
|
||||
acceptance("Managing Group Membership", function (needs) {
|
||||
needs.user();
|
||||
needs.pretender((server, helper) => {
|
||||
server.get("/associated_groups", () =>
|
||||
helper.response({
|
||||
associated_groups: [
|
||||
{
|
||||
id: 123,
|
||||
name: "test-group",
|
||||
provider_name: "google_oauth2",
|
||||
label: "google_oauth2:test-group",
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test("As an admin", async function (assert) {
|
||||
updateCurrentUser({ can_create_group: true });
|
||||
@ -94,6 +109,36 @@ acceptance("Managing Group Membership", function (needs) {
|
||||
assert.strictEqual(emailDomains.header().value(), "foo.com");
|
||||
});
|
||||
|
||||
test("As an admin on a site that can associate groups", async function (assert) {
|
||||
let site = Site.current();
|
||||
site.set("can_associate_groups", true);
|
||||
updateCurrentUser({ can_create_group: true });
|
||||
|
||||
await visit("/g/alternative-group/manage/membership");
|
||||
|
||||
const associatedGroups = selectKit(
|
||||
".group-form-automatic-membership-associated-groups"
|
||||
);
|
||||
await associatedGroups.expand();
|
||||
await associatedGroups.selectRowByName("google_oauth2:test-group");
|
||||
await associatedGroups.keyboard("enter");
|
||||
|
||||
assert.equal(associatedGroups.header().name(), "google_oauth2:test-group");
|
||||
});
|
||||
|
||||
test("As an admin on a site that can't associate groups", async function (assert) {
|
||||
let site = Site.current();
|
||||
site.set("can_associate_groups", false);
|
||||
updateCurrentUser({ can_create_group: true });
|
||||
|
||||
await visit("/g/alternative-group/manage/membership");
|
||||
|
||||
assert.ok(
|
||||
!exists('label[for="automatic_membership_associated_groups"]'),
|
||||
"it should not display associated groups automatic membership label"
|
||||
);
|
||||
});
|
||||
|
||||
test("As a group owner", async function (assert) {
|
||||
updateCurrentUser({ moderator: false, admin: false });
|
||||
|
||||
@ -104,6 +149,11 @@ acceptance("Managing Group Membership", function (needs) {
|
||||
"it should not display automatic membership label"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
!exists('label[for="automatic_membership_associated_groups"]'),
|
||||
"it should not display associated groups automatic membership label"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
!exists(".groups-form-automatic-membership-retroactive"),
|
||||
"it should not display automatic membership retroactive checkbox"
|
||||
|
||||
@ -8,7 +8,7 @@ import {
|
||||
queryAll,
|
||||
updateCurrentUser,
|
||||
} from "discourse/tests/helpers/qunit-helpers";
|
||||
import { click, currentURL, visit } from "@ember/test-helpers";
|
||||
import { click, currentURL, fillIn, visit } from "@ember/test-helpers";
|
||||
import { test } from "qunit";
|
||||
|
||||
acceptance("Tags", function (needs) {
|
||||
@ -350,6 +350,10 @@ acceptance("Tag info", function (needs) {
|
||||
],
|
||||
});
|
||||
});
|
||||
server.put("/tag/happy-monkey", (request) => {
|
||||
const data = helper.parsePostData(request.requestBody);
|
||||
return helper.response({ tag: { id: data.tag.id } });
|
||||
});
|
||||
|
||||
server.get("/tag/happy-monkey/info", () => {
|
||||
return helper.response({
|
||||
@ -441,7 +445,7 @@ acceptance("Tag info", function (needs) {
|
||||
await click("#show-tag-info");
|
||||
assert.ok(exists(".tag-info .tag-name"), "show tag");
|
||||
|
||||
await click("#edit-tag");
|
||||
await click(".edit-tag");
|
||||
assert.strictEqual(
|
||||
query("#edit-name").value,
|
||||
"happy-monkey",
|
||||
@ -452,6 +456,23 @@ acceptance("Tag info", function (needs) {
|
||||
"happy monkey description",
|
||||
"it displays original tag description"
|
||||
);
|
||||
|
||||
await fillIn("#edit-description", "new description");
|
||||
await click(".submit-edit");
|
||||
assert.strictEqual(
|
||||
currentURL(),
|
||||
"/tag/happy-monkey",
|
||||
"it doesn't change URL"
|
||||
);
|
||||
|
||||
await click(".edit-tag");
|
||||
await fillIn("#edit-name", "happy-monkey2");
|
||||
await click(".submit-edit");
|
||||
assert.strictEqual(
|
||||
currentURL(),
|
||||
"/tag/happy-monkey2",
|
||||
"it changes URL to new tag path"
|
||||
);
|
||||
});
|
||||
|
||||
test("can filter tags page by category", async function (assert) {
|
||||
@ -470,7 +491,7 @@ acceptance("Tag info", function (needs) {
|
||||
assert.strictEqual(count("#show-tag-info"), 1);
|
||||
|
||||
await click("#show-tag-info");
|
||||
assert.ok(exists("#edit-tag"), "can rename tag");
|
||||
assert.ok(exists(".edit-tag"), "can rename tag");
|
||||
assert.ok(exists("#edit-synonyms"), "can edit synonyms");
|
||||
assert.ok(exists("#delete-tag"), "can delete tag");
|
||||
|
||||
|
||||
@ -728,6 +728,100 @@ third line`
|
||||
assert.strictEqual(this.value, "red yellow blue");
|
||||
});
|
||||
|
||||
async function indentSelection(container, direction) {
|
||||
await container
|
||||
.lookup("service:app-events")
|
||||
.trigger("composer:indent-selected-text", direction);
|
||||
}
|
||||
|
||||
composerTestCase(
|
||||
"indents a single line of text to the right",
|
||||
async function (assert, textarea) {
|
||||
this.set("value", "Hello world");
|
||||
setTextareaSelection(textarea, 0, textarea.value.length);
|
||||
await indentSelection(this.container, "right");
|
||||
|
||||
assert.strictEqual(
|
||||
this.value,
|
||||
" Hello world",
|
||||
"a single line of selection is indented correctly"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
composerTestCase(
|
||||
"de-indents a single line of text to the left",
|
||||
async function (assert, textarea) {
|
||||
this.set("value", " Hello world");
|
||||
setTextareaSelection(textarea, 0, textarea.value.length);
|
||||
await indentSelection(this.container, "left");
|
||||
|
||||
assert.strictEqual(
|
||||
this.value,
|
||||
"Hello world",
|
||||
"a single line of selection is deindented correctly"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
composerTestCase(
|
||||
"indents multiple lines of text to the right",
|
||||
async function (assert, textarea) {
|
||||
this.set("value", " Hello world\nThis is me");
|
||||
setTextareaSelection(textarea, 2, textarea.value.length);
|
||||
await indentSelection(this.container, "right");
|
||||
|
||||
assert.strictEqual(
|
||||
this.value,
|
||||
" Hello world\n This is me",
|
||||
"multiple lines are indented correctly without selecting preceding space"
|
||||
);
|
||||
|
||||
this.set("value", " Hello world\nThis is me");
|
||||
setTextareaSelection(textarea, 0, textarea.value.length);
|
||||
await indentSelection(this.container, "right");
|
||||
|
||||
assert.strictEqual(
|
||||
this.value,
|
||||
" Hello world\n This is me",
|
||||
"multiple lines are indented correctly with selecting preceding space"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
composerTestCase(
|
||||
"de-indents multiple lines of text to the left",
|
||||
async function (assert, textarea) {
|
||||
this.set("value", " Hello world\nThis is me");
|
||||
setTextareaSelection(textarea, 2, textarea.value.length);
|
||||
await indentSelection(this.container, "left");
|
||||
|
||||
assert.strictEqual(
|
||||
this.value,
|
||||
"Hello world\nThis is me",
|
||||
"multiple lines are de-indented correctly without selecting preceding space"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
composerTestCase(
|
||||
"detects the indentation character (tab vs. string) and uses that",
|
||||
async function (assert, textarea) {
|
||||
this.set(
|
||||
"value",
|
||||
"```\nfunc init() {\n strings = generateStrings()\n}\n```"
|
||||
);
|
||||
setTextareaSelection(textarea, 4, textarea.value.length - 4);
|
||||
await indentSelection(this.container, "right");
|
||||
|
||||
assert.strictEqual(
|
||||
this.value,
|
||||
"```\n func init() {\n strings = generateStrings()\n }\n```",
|
||||
"detects the prevalent indentation character and uses that (tab)"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
async function paste(element, text) {
|
||||
let e = new Event("paste", { cancelable: true });
|
||||
e.clipboardData = { getData: () => text };
|
||||
@ -833,6 +927,32 @@ third line`
|
||||
}
|
||||
);
|
||||
|
||||
testCase(
|
||||
`pasting a url onto a selection that contains urls and other content will use default paste behavior`,
|
||||
async function (assert, textarea) {
|
||||
this.set("value", "Try https://www.discourse.org");
|
||||
setTextareaSelection(textarea, 0, 29);
|
||||
const element = query(".d-editor");
|
||||
const event = await paste(element, "https://www.discourse.com/");
|
||||
// Synthetic paste events do not manipulate document content.
|
||||
assert.strictEqual(this.value, "Try https://www.discourse.org");
|
||||
assert.strictEqual(event.defaultPrevented, false);
|
||||
}
|
||||
);
|
||||
|
||||
testCase(
|
||||
`pasting a url onto a selection that contains bbcode-like tags will use default paste behavior`,
|
||||
async function (assert, textarea) {
|
||||
this.set("value", "hello [url=foobar]foobar[/url]");
|
||||
setTextareaSelection(textarea, 0, 30);
|
||||
const element = query(".d-editor");
|
||||
const event = await paste(element, "https://www.discourse.com/");
|
||||
// Synthetic paste events do not manipulate document content.
|
||||
assert.strictEqual(this.value, "hello [url=foobar]foobar[/url]");
|
||||
assert.strictEqual(event.defaultPrevented, false);
|
||||
}
|
||||
);
|
||||
|
||||
(() => {
|
||||
// Tests to check cursor/selection after replace-text event.
|
||||
const BEFORE = "red green blue";
|
||||
|
||||
@ -3,12 +3,16 @@ import {
|
||||
setup as setupIt,
|
||||
} from "pretty-text/engines/discourse-markdown-it";
|
||||
import { deepMerge } from "discourse-common/lib/object";
|
||||
import deprecated from "discourse-common/lib/deprecated";
|
||||
|
||||
export function registerOption() {
|
||||
// TODO next major version deprecate this
|
||||
// if (window.console) {
|
||||
// window.console.log("registerOption is deprecated");
|
||||
// }
|
||||
deprecated(
|
||||
"`registerOption() from `pretty-text` is deprecated. Use `helper.registerOptions()` instead.",
|
||||
{
|
||||
since: "2.8.0.beta9",
|
||||
dropFrom: "2.9.0.beta1",
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function buildOptions(state) {
|
||||
|
||||
@ -208,11 +208,6 @@ export default DropdownSelectBoxComponent.extend({
|
||||
});
|
||||
}
|
||||
|
||||
let showCreateTopic = false;
|
||||
if (this.action === CREATE_SHARED_DRAFT) {
|
||||
showCreateTopic = true;
|
||||
}
|
||||
|
||||
if (this.action === CREATE_TOPIC) {
|
||||
if (this.site.shared_drafts_category_id) {
|
||||
// Shared Drafts Choice
|
||||
@ -223,24 +218,6 @@ export default DropdownSelectBoxComponent.extend({
|
||||
id: "shared_draft",
|
||||
});
|
||||
}
|
||||
|
||||
// Edge case: If personal messages are disabled, it is possible to have
|
||||
// no items which still renders a button that pops up nothing. In this
|
||||
// case, add an option for what you're currently doing.
|
||||
if (items.length === 0) {
|
||||
showCreateTopic = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (showCreateTopic) {
|
||||
items.push({
|
||||
name: I18n.t("composer.composer_actions.create_topic.label"),
|
||||
description: I18n.t(
|
||||
"composer.composer_actions.reply_as_new_topic.desc"
|
||||
),
|
||||
icon: "share",
|
||||
id: "create_topic",
|
||||
});
|
||||
}
|
||||
|
||||
const showToggleTopicBump =
|
||||
@ -256,6 +233,17 @@ export default DropdownSelectBoxComponent.extend({
|
||||
});
|
||||
}
|
||||
|
||||
if (items.length === 0) {
|
||||
items.push({
|
||||
name: I18n.t("composer.composer_actions.create_topic.label"),
|
||||
description: I18n.t(
|
||||
"composer.composer_actions.reply_as_new_topic.desc"
|
||||
),
|
||||
icon: "share",
|
||||
id: "create_topic",
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
},
|
||||
|
||||
|
||||
@ -289,6 +289,9 @@ export default Component.extend(
|
||||
focusAfterOnChange: true,
|
||||
triggerOnChangeOnTab: true,
|
||||
autofocus: false,
|
||||
placementStrategy: null,
|
||||
mobilePlacementStrategy: null,
|
||||
desktopPlacementStrategy: null,
|
||||
},
|
||||
|
||||
autoFilterable: computed("content.[]", "selectKit.filter", function () {
|
||||
@ -711,7 +714,7 @@ export default Component.extend(
|
||||
rowContainer = this.element.querySelector(".select-kit-row.is-none");
|
||||
}
|
||||
|
||||
rowContainer && rowContainer.focus({ preventScroll });
|
||||
rowContainer?.focus({ preventScroll });
|
||||
},
|
||||
|
||||
_highlightLast() {
|
||||
@ -849,36 +852,28 @@ export default Component.extend(
|
||||
}
|
||||
|
||||
this.selectKit.mainElement().open = true;
|
||||
|
||||
this.clearErrors();
|
||||
|
||||
const inModal = this.element.closest("#discourse-modal");
|
||||
|
||||
this.selectKit.onOpen(event);
|
||||
|
||||
if (!this.popper) {
|
||||
const inModal = this.element.closest("#discourse-modal");
|
||||
const anchor = document.querySelector(
|
||||
`#${this.selectKit.uniqueID}-header`
|
||||
);
|
||||
const popper = document.querySelector(
|
||||
`#${this.selectKit.uniqueID}-body`
|
||||
);
|
||||
|
||||
const placementStrategy =
|
||||
this.capabilities?.isIpadOS || this.site?.mobileView
|
||||
? "absolute"
|
||||
: "fixed";
|
||||
const verticalOffset = 3;
|
||||
const strategy = this._computePlacementStrategy();
|
||||
|
||||
this.popper = createPopper(anchor, popper, {
|
||||
eventsEnabled: false,
|
||||
strategy: placementStrategy,
|
||||
strategy,
|
||||
placement: this.selectKit.options.placement,
|
||||
modifiers: [
|
||||
{
|
||||
name: "offset",
|
||||
options: {
|
||||
offset: [0, verticalOffset],
|
||||
offset: [0, 3],
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -888,7 +883,11 @@ export default Component.extend(
|
||||
fn({ state }) {
|
||||
if (!inModal) {
|
||||
let { x } = state.elements.reference.getBoundingClientRect();
|
||||
state.modifiersData.popperOffsets.x = -x + 10;
|
||||
if (strategy === "fixed") {
|
||||
state.modifiersData.popperOffsets.x = 0 + 10;
|
||||
} else {
|
||||
state.modifiersData.popperOffsets.x = -x + 10;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
@ -990,7 +989,7 @@ export default Component.extend(
|
||||
}
|
||||
|
||||
if (highlighted) {
|
||||
this._scrollToRow(highlighted);
|
||||
this._scrollToRow(highlighted, false);
|
||||
this.set("selectKit.highlighted", highlighted);
|
||||
}
|
||||
}
|
||||
@ -1036,6 +1035,24 @@ export default Component.extend(
|
||||
this._deprecateOptions();
|
||||
},
|
||||
|
||||
_computePlacementStrategy() {
|
||||
let placementStrategy = this.selectKit.options.placementStrategy;
|
||||
|
||||
if (placementStrategy) {
|
||||
return placementStrategy;
|
||||
}
|
||||
|
||||
if (this.capabilities?.isIpadOS || this.site?.mobileView) {
|
||||
placementStrategy =
|
||||
this.selectKit.options.mobilePlacementStrategy || "absolute";
|
||||
} else {
|
||||
placementStrategy =
|
||||
this.selectKit.options.desktopPlacementStrategy || "fixed";
|
||||
}
|
||||
|
||||
return placementStrategy;
|
||||
},
|
||||
|
||||
_deprecated(text) {
|
||||
const discourseSetup = document.getElementById("data-discourse-setup");
|
||||
if (
|
||||
|
||||
@ -56,6 +56,10 @@ export default Component.extend(UtilsMixin, {
|
||||
}
|
||||
},
|
||||
|
||||
mouseDown() {
|
||||
return false;
|
||||
},
|
||||
|
||||
click(event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
@ -4,3 +4,40 @@ define("@popperjs/core", ["exports"], function (__exports__) {
|
||||
__exports__.defaultModifiers = window.Popper.defaultModifiers;
|
||||
__exports__.popperGenerator = window.Popper.popperGenerator;
|
||||
});
|
||||
|
||||
define("@uppy/core", ["exports"], function (__exports__) {
|
||||
__exports__.default = window.Uppy.Core;
|
||||
__exports__.BasePlugin = window.Uppy.Core.BasePlugin;
|
||||
});
|
||||
|
||||
define("@uppy/aws-s3", ["exports"], function (__exports__) {
|
||||
__exports__.default = window.Uppy.AwsS3;
|
||||
});
|
||||
|
||||
define("@uppy/aws-s3-multipart", ["exports"], function (__exports__) {
|
||||
__exports__.default = window.Uppy.AwsS3Multipart;
|
||||
});
|
||||
|
||||
define("@uppy/xhr-upload", ["exports"], function (__exports__) {
|
||||
__exports__.default = window.Uppy.XHRUpload;
|
||||
});
|
||||
|
||||
define("@uppy/drop-target", ["exports"], function (__exports__) {
|
||||
__exports__.default = window.Uppy.DropTarget;
|
||||
});
|
||||
|
||||
define("@uppy/utils/lib/delay", ["exports"], function (__exports__) {
|
||||
__exports__.default = window.Uppy.Utils.delay;
|
||||
});
|
||||
|
||||
define("@uppy/utils/lib/EventTracker", ["exports"], function (__exports__) {
|
||||
__exports__.default = window.Uppy.Utils.EventTracker;
|
||||
});
|
||||
|
||||
define("@uppy/utils/lib/AbortController", ["exports"], function (__exports__) {
|
||||
__exports__.AbortController =
|
||||
window.Uppy.Utils.AbortControllerLib.AbortController;
|
||||
__exports__.AbortSignal = window.Uppy.Utils.AbortControllerLib.AbortSignal;
|
||||
__exports__.createAbortError =
|
||||
window.Uppy.Utils.AbortControllerLib.createAbortError;
|
||||
});
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
//= require template_include.js
|
||||
//= require jquery.ui.widget.js
|
||||
//= require jquery.fileupload.js
|
||||
//= require uppy.js
|
||||
//= require bootstrap-modal.js
|
||||
//= require bootbox.js
|
||||
//= require virtual-dom
|
||||
|
||||
@ -1,10 +1,14 @@
|
||||
import Component from "@ember/component";
|
||||
import { warn } from "@ember/debug";
|
||||
import I18n from "I18n";
|
||||
import { dasherize } from "@ember/string";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import { getOwner } from "discourse-common/lib/get-owner";
|
||||
import { getToken } from "wizard/lib/ajax";
|
||||
import getUrl from "discourse-common/lib/get-url";
|
||||
import Uppy from "@uppy/core";
|
||||
import DropTarget from "@uppy/drop-target";
|
||||
import XHRUpload from "@uppy/xhr-upload";
|
||||
|
||||
export default Component.extend({
|
||||
classNames: ["wizard-image-row"],
|
||||
@ -19,37 +23,63 @@ export default Component.extend({
|
||||
|
||||
didInsertElement() {
|
||||
this._super(...arguments);
|
||||
this.setupUploads();
|
||||
},
|
||||
|
||||
const $upload = $(this.element);
|
||||
|
||||
setupUploads() {
|
||||
const id = this.get("field.id");
|
||||
|
||||
$upload.fileupload({
|
||||
url: getUrl("/uploads.json"),
|
||||
formData: {
|
||||
synchronous: true,
|
||||
type: `wizard_${id}`,
|
||||
authenticity_token: getToken(),
|
||||
},
|
||||
dataType: "json",
|
||||
dropZone: $upload,
|
||||
this._uppyInstance = new Uppy({
|
||||
id: `wizard-field-image-${id}`,
|
||||
meta: { upload_type: `wizard_${id}` },
|
||||
autoProceed: true,
|
||||
});
|
||||
|
||||
$upload.on("fileuploadsubmit", () => this.set("uploading", true));
|
||||
this._uppyInstance.use(XHRUpload, {
|
||||
endpoint: getUrl("/uploads.json"),
|
||||
headers: {
|
||||
"X-CSRF-Token": getToken(),
|
||||
},
|
||||
});
|
||||
|
||||
$upload.on("fileuploaddone", (e, response) => {
|
||||
this.set("field.value", response.result.url);
|
||||
this._uppyInstance.use(DropTarget, { target: this.element });
|
||||
|
||||
this._uppyInstance.on("upload", () => {
|
||||
this.set("uploading", true);
|
||||
});
|
||||
|
||||
this._uppyInstance.on("upload-success", (file, response) => {
|
||||
this.set("field.value", response.body.url);
|
||||
this.set("uploading", false);
|
||||
});
|
||||
|
||||
$upload.on("fileuploadfail", (e, response) => {
|
||||
this._uppyInstance.on("upload-error", (file, error, response) => {
|
||||
let message = I18n.t("wizard.upload_error");
|
||||
if (response.jqXHR.responseJSON && response.jqXHR.responseJSON.errors) {
|
||||
message = response.jqXHR.responseJSON.errors.join("\n");
|
||||
if (response.body.errors) {
|
||||
message = response.body.errors.join("\n");
|
||||
}
|
||||
|
||||
window.bootbox.alert(message);
|
||||
this.set("uploading", false);
|
||||
});
|
||||
|
||||
this.element
|
||||
.querySelector(".wizard-hidden-upload-field")
|
||||
.addEventListener("change", (event) => {
|
||||
const files = Array.from(event.target.files);
|
||||
files.forEach((file) => {
|
||||
try {
|
||||
this._uppyInstance.addFile({
|
||||
source: `${this.id} file input`,
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
data: file,
|
||||
});
|
||||
} catch (err) {
|
||||
warn(`error adding files to uppy: ${err}`, {
|
||||
id: "discourse.upload.uppy-add-files-error",
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@ -1317,6 +1317,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz#8da5c6530915653f3a1f38fd5f101d8c3f8079c5"
|
||||
integrity sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==
|
||||
|
||||
"@tootallnate/once@2":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf"
|
||||
integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==
|
||||
|
||||
"@transloadit/prettier-bytes@0.0.7":
|
||||
version "0.0.7"
|
||||
resolved "https://registry.yarnpkg.com/@transloadit/prettier-bytes/-/prettier-bytes-0.0.7.tgz#cdb5399f445fdd606ed833872fa0cabdbc51686b"
|
||||
@ -1715,11 +1720,23 @@ acorn@^8.1.0:
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.1.1.tgz#fb0026885b9ac9f48bac1e185e4af472971149ff"
|
||||
integrity sha512-xYiIVjNuqtKXMxlRMDc6mZUhXehod4a3gbZ1qRlM7icK4EbxUFNLhWoPblCvFtB2Y9CIqHP3CF/rdxLItaQv8g==
|
||||
|
||||
acorn@^8.5.0:
|
||||
version "8.6.0"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.6.0.tgz#e3692ba0eb1a0c83eaa4f37f5fa7368dd7142895"
|
||||
integrity sha512-U1riIR+lBSNi3IbxtaHOIKdH8sLFv3NYfNv8sg7ZsNhcfl4HF2++BfqqrNAxoCLQW1iiylOj76ecnaUxz+z9yw==
|
||||
|
||||
after@0.8.2:
|
||||
version "0.8.2"
|
||||
resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f"
|
||||
integrity sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=
|
||||
|
||||
agent-base@6:
|
||||
version "6.0.2"
|
||||
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
|
||||
integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==
|
||||
dependencies:
|
||||
debug "4"
|
||||
|
||||
ajv-errors@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d"
|
||||
@ -4080,7 +4097,7 @@ colors@^1.1.2:
|
||||
resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78"
|
||||
integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==
|
||||
|
||||
combined-stream@^1.0.6, combined-stream@~1.0.6:
|
||||
combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
|
||||
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
|
||||
@ -4405,6 +4422,11 @@ cssom@^0.4.4:
|
||||
resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.4.4.tgz#5a66cf93d2d0b661d80bf6a44fb65f5c2e4e0a10"
|
||||
integrity sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==
|
||||
|
||||
cssom@^0.5.0:
|
||||
version "0.5.0"
|
||||
resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.5.0.tgz#d254fa92cd8b6fbd83811b9fbaed34663cc17c36"
|
||||
integrity sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==
|
||||
|
||||
cssom@~0.3.6:
|
||||
version "0.3.8"
|
||||
resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a"
|
||||
@ -4443,6 +4465,15 @@ data-urls@^2.0.0:
|
||||
whatwg-mimetype "^2.3.0"
|
||||
whatwg-url "^8.0.0"
|
||||
|
||||
data-urls@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-3.0.1.tgz#597fc2ae30f8bc4dbcf731fcd1b1954353afc6f8"
|
||||
integrity sha512-Ds554NeT5Gennfoo9KN50Vh6tpgtvYEwraYjejXnyTpu1C7oXKxdFk75REooENHE8ndTVOJuv+BEs4/J/xcozw==
|
||||
dependencies:
|
||||
abab "^2.0.3"
|
||||
whatwg-mimetype "^3.0.0"
|
||||
whatwg-url "^10.0.0"
|
||||
|
||||
debug@2.6.9, debug@^2.1.0, debug@^2.1.1, debug@^2.1.3, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9:
|
||||
version "2.6.9"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
|
||||
@ -4450,6 +4481,13 @@ debug@2.6.9, debug@^2.1.0, debug@^2.1.1, debug@^2.1.3, debug@^2.2.0, debug@^2.3.
|
||||
dependencies:
|
||||
ms "2.0.0"
|
||||
|
||||
debug@4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.2.0:
|
||||
version "4.3.2"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b"
|
||||
integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==
|
||||
dependencies:
|
||||
ms "2.1.2"
|
||||
|
||||
debug@^3.0.1, debug@^3.1.0, debug@^3.1.1:
|
||||
version "3.2.7"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
|
||||
@ -4457,13 +4495,6 @@ debug@^3.0.1, debug@^3.1.0, debug@^3.1.1:
|
||||
dependencies:
|
||||
ms "^2.1.1"
|
||||
|
||||
debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.2.0:
|
||||
version "4.3.2"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b"
|
||||
integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==
|
||||
dependencies:
|
||||
ms "2.1.2"
|
||||
|
||||
debug@~3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
|
||||
@ -4483,6 +4514,11 @@ decimal.js@^10.2.1:
|
||||
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.2.1.tgz#238ae7b0f0c793d3e3cea410108b35a2c01426a3"
|
||||
integrity sha512-KaL7+6Fw6i5A2XSnsbhm/6B+NuEA7TZ4vqxnd5tXz9sbKtrN9Srj8ab4vKVdK8YAqZO9P1kg45Y6YLoduPf+kw==
|
||||
|
||||
decimal.js@^10.3.1:
|
||||
version "10.3.1"
|
||||
resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.3.1.tgz#d8c3a444a9c6774ba60ca6ad7261c3a94fd5e783"
|
||||
integrity sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ==
|
||||
|
||||
decode-uri-component@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
|
||||
@ -4653,6 +4689,13 @@ domexception@^2.0.1:
|
||||
dependencies:
|
||||
webidl-conversions "^5.0.0"
|
||||
|
||||
domexception@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/domexception/-/domexception-4.0.0.tgz#4ad1be56ccadc86fc76d033353999a8037d03673"
|
||||
integrity sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==
|
||||
dependencies:
|
||||
webidl-conversions "^7.0.0"
|
||||
|
||||
dot-case@^3.0.4:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751"
|
||||
@ -6391,6 +6434,15 @@ forever-agent@~0.6.1:
|
||||
resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
|
||||
integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=
|
||||
|
||||
form-data@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
|
||||
integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
|
||||
dependencies:
|
||||
asynckit "^0.4.0"
|
||||
combined-stream "^1.0.8"
|
||||
mime-types "^2.1.12"
|
||||
|
||||
form-data@~2.3.2:
|
||||
version "2.3.3"
|
||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6"
|
||||
@ -7122,6 +7174,13 @@ html-encoding-sniffer@^2.0.1:
|
||||
dependencies:
|
||||
whatwg-encoding "^1.0.5"
|
||||
|
||||
html-encoding-sniffer@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz#2cb1a8cf0db52414776e5b2a7a04d5dd98158de9"
|
||||
integrity sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==
|
||||
dependencies:
|
||||
whatwg-encoding "^2.0.0"
|
||||
|
||||
html-entities@^2.1.0:
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.3.2.tgz#760b404685cb1d794e4f4b744332e3b00dcfe488"
|
||||
@ -7169,6 +7228,15 @@ http-parser-js@>=0.5.1:
|
||||
resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.3.tgz#01d2709c79d41698bb01d4decc5e9da4e4a033d9"
|
||||
integrity sha512-t7hjvef/5HEK7RWTdUzVUhl8zkEu+LlaE0IYzdMuvbSDipxBRpOn4Uhw8ZyECEa808iVT8XCjzo6xmYt4CiLZg==
|
||||
|
||||
http-proxy-agent@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43"
|
||||
integrity sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==
|
||||
dependencies:
|
||||
"@tootallnate/once" "2"
|
||||
agent-base "6"
|
||||
debug "4"
|
||||
|
||||
http-proxy@^1.13.1, http-proxy@^1.18.1:
|
||||
version "1.18.1"
|
||||
resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549"
|
||||
@ -7192,6 +7260,14 @@ https-browserify@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
|
||||
integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=
|
||||
|
||||
https-proxy-agent@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2"
|
||||
integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==
|
||||
dependencies:
|
||||
agent-base "6"
|
||||
debug "4"
|
||||
|
||||
https@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/https/-/https-1.0.0.tgz#3c37c7ae1a8eeb966904a2ad1e975a194b7ed3a4"
|
||||
@ -7209,6 +7285,13 @@ iconv-lite@0.4.24, iconv-lite@^0.4.24:
|
||||
dependencies:
|
||||
safer-buffer ">= 2.1.2 < 3"
|
||||
|
||||
iconv-lite@0.6.3:
|
||||
version "0.6.3"
|
||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
|
||||
integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
|
||||
dependencies:
|
||||
safer-buffer ">= 2.1.2 < 3.0.0"
|
||||
|
||||
ieee754@^1.1.4:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
|
||||
@ -7584,7 +7667,7 @@ is-plain-object@^2.0.3, is-plain-object@^2.0.4:
|
||||
dependencies:
|
||||
isobject "^3.0.1"
|
||||
|
||||
is-potential-custom-element-name@^1.0.0:
|
||||
is-potential-custom-element-name@^1.0.0, is-potential-custom-element-name@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5"
|
||||
integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==
|
||||
@ -7803,6 +7886,39 @@ jsdom@^16.4.0:
|
||||
ws "^7.4.4"
|
||||
xml-name-validator "^3.0.0"
|
||||
|
||||
jsdom@^18.1.1:
|
||||
version "18.1.1"
|
||||
resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-18.1.1.tgz#15ec896f5ab7df9669a62375606f47c8c09551aa"
|
||||
integrity sha512-NmJQbjQ/gpS/1at/ce3nCx89HbXL/f5OcenBe8wU1Eik0ROhyUc3LtmG3567dEHAGXkN8rmILW/qtCOPxPHQJw==
|
||||
dependencies:
|
||||
abab "^2.0.5"
|
||||
acorn "^8.5.0"
|
||||
acorn-globals "^6.0.0"
|
||||
cssom "^0.5.0"
|
||||
cssstyle "^2.3.0"
|
||||
data-urls "^3.0.1"
|
||||
decimal.js "^10.3.1"
|
||||
domexception "^4.0.0"
|
||||
escodegen "^2.0.0"
|
||||
form-data "^4.0.0"
|
||||
html-encoding-sniffer "^3.0.0"
|
||||
http-proxy-agent "^5.0.0"
|
||||
https-proxy-agent "^5.0.0"
|
||||
is-potential-custom-element-name "^1.0.1"
|
||||
nwsapi "^2.2.0"
|
||||
parse5 "6.0.1"
|
||||
saxes "^5.0.1"
|
||||
symbol-tree "^3.2.4"
|
||||
tough-cookie "^4.0.0"
|
||||
w3c-hr-time "^1.0.2"
|
||||
w3c-xmlserializer "^3.0.0"
|
||||
webidl-conversions "^7.0.0"
|
||||
whatwg-encoding "^2.0.0"
|
||||
whatwg-mimetype "^3.0.0"
|
||||
whatwg-url "^10.0.0"
|
||||
ws "^8.2.3"
|
||||
xml-name-validator "^4.0.0"
|
||||
|
||||
jsesc@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b"
|
||||
@ -10208,7 +10324,7 @@ safe-regex@^1.1.0:
|
||||
dependencies:
|
||||
ret "~0.1.10"
|
||||
|
||||
"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
|
||||
"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
||||
@ -11272,6 +11388,13 @@ tr46@^2.0.2:
|
||||
dependencies:
|
||||
punycode "^2.1.1"
|
||||
|
||||
tr46@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/tr46/-/tr46-3.0.0.tgz#555c4e297a950617e8eeddef633c87d4d9d6cbf9"
|
||||
integrity sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==
|
||||
dependencies:
|
||||
punycode "^2.1.1"
|
||||
|
||||
tr46@~0.0.3:
|
||||
version "0.0.3"
|
||||
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
|
||||
@ -11688,6 +11811,13 @@ w3c-xmlserializer@^2.0.0:
|
||||
dependencies:
|
||||
xml-name-validator "^3.0.0"
|
||||
|
||||
w3c-xmlserializer@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-3.0.0.tgz#06cdc3eefb7e4d0b20a560a5a3aeb0d2d9a65923"
|
||||
integrity sha512-3WFqGEgSXIyGhOmAFtlicJNMjEps8b1MG31NCA0/vOF9+nKMUW1ckhi9cnNHmf88Rzw5V+dwIwsm2C7X8k9aQg==
|
||||
dependencies:
|
||||
xml-name-validator "^4.0.0"
|
||||
|
||||
walk-sync@^0.2.5:
|
||||
version "0.2.7"
|
||||
resolved "https://registry.yarnpkg.com/walk-sync/-/walk-sync-0.2.7.tgz#b49be4ee6867657aeb736978b56a29d10fa39969"
|
||||
@ -11785,6 +11915,11 @@ webidl-conversions@^6.1.0:
|
||||
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz#9111b4d7ea80acd40f5270d666621afa78b69514"
|
||||
integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==
|
||||
|
||||
webidl-conversions@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a"
|
||||
integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==
|
||||
|
||||
webpack-sources@^1.4.0, webpack-sources@^1.4.1:
|
||||
version "1.4.3"
|
||||
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933"
|
||||
@ -11843,11 +11978,31 @@ whatwg-encoding@^1.0.5:
|
||||
dependencies:
|
||||
iconv-lite "0.4.24"
|
||||
|
||||
whatwg-encoding@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz#e7635f597fd87020858626805a2729fa7698ac53"
|
||||
integrity sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==
|
||||
dependencies:
|
||||
iconv-lite "0.6.3"
|
||||
|
||||
whatwg-mimetype@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf"
|
||||
integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==
|
||||
|
||||
whatwg-mimetype@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz#5fa1a7623867ff1af6ca3dc72ad6b8a4208beba7"
|
||||
integrity sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==
|
||||
|
||||
whatwg-url@^10.0.0:
|
||||
version "10.0.0"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-10.0.0.tgz#37264f720b575b4a311bd4094ed8c760caaa05da"
|
||||
integrity sha512-CLxxCmdUby142H5FZzn4D8ikO1cmypvXVQktsgosNy4a4BHrDHeciBBGZhb0bNoR5/MltoCatso+vFjjGx8t0w==
|
||||
dependencies:
|
||||
tr46 "^3.0.0"
|
||||
webidl-conversions "^7.0.0"
|
||||
|
||||
whatwg-url@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
|
||||
@ -11987,6 +12142,11 @@ ws@^7.4.4, ws@~7.4.2:
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.4.tgz#383bc9742cb202292c9077ceab6f6047b17f2d59"
|
||||
integrity sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw==
|
||||
|
||||
ws@^8.2.3:
|
||||
version "8.3.0"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-8.3.0.tgz#7185e252c8973a60d57170175ff55fdbd116070d"
|
||||
integrity sha512-Gs5EZtpqZzLvmIM59w4igITU57lrtYVFneaa434VROv4thzJyV6UjIL3D42lslWlI+D4KzLYnxSwtfuiO79sNw==
|
||||
|
||||
x-is-array@0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/x-is-array/-/x-is-array-0.1.0.tgz#de520171d47b3f416f5587d629b89d26b12dc29d"
|
||||
@ -12007,6 +12167,11 @@ xml-name-validator@^3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
|
||||
integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==
|
||||
|
||||
xml-name-validator@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz#79a006e2e63149a8600f15430f0a4725d1524835"
|
||||
integrity sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==
|
||||
|
||||
xmlchars@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
|
||||
|
||||
@ -372,7 +372,7 @@ table {
|
||||
}
|
||||
|
||||
.profiler-results.profiler-left {
|
||||
top: 60px !important;
|
||||
top: var(--header-offset) !important;
|
||||
}
|
||||
|
||||
.flex-center-align {
|
||||
|
||||
@ -340,51 +340,83 @@ section.tag-info {
|
||||
|
||||
.edit-tag-wrapper {
|
||||
display: flex;
|
||||
|
||||
input {
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 1em;
|
||||
#edit-name {
|
||||
flex: 1 1 auto;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
#edit-description {
|
||||
flex: 10 1 auto;
|
||||
}
|
||||
.edit-controls {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.tag-name-wrapper,
|
||||
|
||||
.tag-name-wrapper {
|
||||
display: flex;
|
||||
font-size: var(--font-up-4);
|
||||
align-items: baseline;
|
||||
button {
|
||||
font-size: var(--font-down-1);
|
||||
.d-icon {
|
||||
color: var(--primary-high);
|
||||
}
|
||||
.discourse-no-touch & {
|
||||
&:hover {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tag-description-wrapper {
|
||||
display: flex;
|
||||
}
|
||||
.tag-name-wrapper a {
|
||||
color: var(--primary-high);
|
||||
margin-left: 0.5em;
|
||||
font-size: var(--font-up-1);
|
||||
}
|
||||
|
||||
.tag-name-wrapper a {
|
||||
font-size: var(--font-up-3);
|
||||
}
|
||||
|
||||
.tag-name .discourse-tag {
|
||||
display: block;
|
||||
.tag-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 0.25em;
|
||||
a {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.tag-description-wrapper {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.synonyms-list,
|
||||
.add-synonyms,
|
||||
.tag-associations {
|
||||
margin-top: 1em;
|
||||
@include clearfix;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.synonyms-list {
|
||||
margin: 2em 0 0;
|
||||
.tag-actions {
|
||||
margin-top: 2em;
|
||||
}
|
||||
|
||||
.add-synonyms {
|
||||
margin-top: 1em;
|
||||
div {
|
||||
display: flex;
|
||||
}
|
||||
.ok {
|
||||
margin-left: 0.5em;
|
||||
display: none;
|
||||
}
|
||||
.has-selection + .ok {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.tag-list {
|
||||
margin: 1em 0 0 0;
|
||||
margin: 0.5em 0 1em;
|
||||
padding: 0;
|
||||
border: none;
|
||||
@include clearfix;
|
||||
a {
|
||||
color: var(--primary-medium);
|
||||
margin-left: 0.25em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -482,6 +482,7 @@ aside.quote {
|
||||
}
|
||||
}
|
||||
|
||||
.post-avatar,
|
||||
.topic-avatar,
|
||||
.user-card-avatar {
|
||||
position: relative;
|
||||
@ -714,9 +715,6 @@ aside.quote {
|
||||
font-size: $font-down-2;
|
||||
}
|
||||
}
|
||||
.topic-avatar .poster-avatar-extra {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.map {
|
||||
&:first-of-type {
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 5fr;
|
||||
grid-template-rows: auto auto 1fr auto;
|
||||
grid-row-gap: 20px;
|
||||
grid-gap: 20px;
|
||||
.user-primary-navigation {
|
||||
grid-column-start: 1;
|
||||
grid-column-end: 3;
|
||||
|
||||
@ -21,9 +21,12 @@
|
||||
@extend %nav;
|
||||
@extend .clearfix;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
|
||||
> li {
|
||||
display: flex;
|
||||
float: left;
|
||||
margin-right: 0.5em;
|
||||
|
||||
> a {
|
||||
@ -36,7 +39,7 @@
|
||||
min-height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: background 0.15s;
|
||||
transition: background-color 0.2s, color 0.2s;
|
||||
|
||||
.d-icon {
|
||||
margin-right: 5px;
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
$flair-size: 18px;
|
||||
|
||||
.select-kit.flair-chooser {
|
||||
.select-kit-header,
|
||||
.flair-row {
|
||||
@ -5,22 +7,22 @@
|
||||
align-items: center;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 30px 30px;
|
||||
background-size: $flair-size $flair-size;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-right: 5px;
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
height: $flair-size;
|
||||
width: $flair-size;
|
||||
|
||||
&.rounded {
|
||||
background-size: (30px / 1.4) (30px / 1.4);
|
||||
background-size: ($flair-size / 1.4) ($flair-size / 1.4);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.d-icon {
|
||||
display: block;
|
||||
height: (30px / 1.8);
|
||||
width: (30px / 1.8);
|
||||
height: ($flair-size / 1.8);
|
||||
width: ($flair-size / 1.8);
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,8 +30,4 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.select-kit-header {
|
||||
padding: 2px 4px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -72,7 +72,6 @@
|
||||
touch-action: none;
|
||||
|
||||
.timeline-date-wrapper {
|
||||
float: right;
|
||||
text-align: right;
|
||||
}
|
||||
.post-excerpt {
|
||||
@ -142,7 +141,6 @@
|
||||
border-right-style: solid;
|
||||
border-right-width: 1px;
|
||||
max-width: 120px;
|
||||
margin-top: 2em;
|
||||
|
||||
.timeline-scroller {
|
||||
position: relative;
|
||||
|
||||
@ -1,33 +1,3 @@
|
||||
.group-nav {
|
||||
.group-dropdown {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.group-navigation {
|
||||
width: 15%;
|
||||
background-color: transparent;
|
||||
|
||||
li {
|
||||
border: none;
|
||||
|
||||
a {
|
||||
color: var(--primary-med-or-secondary-high);
|
||||
padding: 8px 0;
|
||||
|
||||
&.active {
|
||||
background-color: transparent;
|
||||
font-weight: bold;
|
||||
color: var(--primary);
|
||||
|
||||
&:after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.group-activity-outlet,
|
||||
.group-messages-outlet,
|
||||
.group-manage-outlet {
|
||||
|
||||
@ -29,7 +29,10 @@
|
||||
|
||||
li {
|
||||
border-bottom: none;
|
||||
|
||||
&:hover,
|
||||
&.active {
|
||||
background: var(--primary-very-low);
|
||||
}
|
||||
&.archive {
|
||||
padding-left: 1.4em;
|
||||
}
|
||||
@ -272,11 +275,6 @@ table.user-invite-list {
|
||||
.show-mores {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
#reply-control .mini-tag-chooser {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.user-messages {
|
||||
|
||||
@ -23,11 +23,14 @@
|
||||
}
|
||||
|
||||
.keyboard-visible &.open {
|
||||
height: calc(var(--composer-vh, 1vh) * 100);
|
||||
height: 100%; // Android: Reduces composer jumpiness when the keyboard toggles
|
||||
}
|
||||
|
||||
.keyboard-visible body.ios-safari-composer-hacks &.open .reply-area {
|
||||
padding-bottom: 0px;
|
||||
.keyboard-visible body.ios-safari-composer-hacks &.open {
|
||||
height: calc(var(--composer-vh, 1vh) * 100);
|
||||
.reply-area {
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.reply-to {
|
||||
|
||||
@ -21,4 +21,7 @@
|
||||
.fps-result {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
.search-link .topic-statuses {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
}
|
||||
|
||||
@ -115,8 +115,10 @@ sub sub {
|
||||
.topic-timeline {
|
||||
.start-date,
|
||||
.now-date {
|
||||
font-size: $font-up-1;
|
||||
padding: 5px;
|
||||
padding: 0.5em 0;
|
||||
}
|
||||
.timeline-scrollarea-wrapper .timeline-date-wrapper {
|
||||
@include ellipsis;
|
||||
}
|
||||
.topic-category {
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
@ -180,6 +180,10 @@ class Admin::GroupsController < Admin::AdminController
|
||||
custom_fields = DiscoursePluginRegistry.editable_group_custom_fields
|
||||
permitted << { custom_fields: custom_fields } unless custom_fields.blank?
|
||||
|
||||
if guardian.can_associate_groups?
|
||||
permitted << { associated_group_ids: [] }
|
||||
end
|
||||
|
||||
permitted = permitted | DiscoursePluginRegistry.group_params
|
||||
|
||||
params.require(:group).permit(permitted)
|
||||
|
||||
@ -59,7 +59,7 @@ class Admin::ThemesController < Admin::AdminController
|
||||
json = JSON::parse(params[:theme].read)
|
||||
theme = json['theme']
|
||||
|
||||
@theme = Theme.new(name: theme["name"], user_id: theme_user.id)
|
||||
@theme = Theme.new(name: theme["name"], user_id: theme_user.id, auto_update: false)
|
||||
theme["theme_fields"]&.each do |field|
|
||||
|
||||
if field["raw_upload"]
|
||||
@ -116,7 +116,14 @@ class Admin::ThemesController < Admin::AdminController
|
||||
update_components = params[:components]
|
||||
match_theme_by_name = !!params[:bundle] && !params.key?(:theme_id) # Old theme CLI behavior, match by name. Remove Jan 2020
|
||||
begin
|
||||
@theme = RemoteTheme.update_zipped_theme(bundle.path, bundle.original_filename, match_theme: match_theme_by_name, user: theme_user, theme_id: theme_id, update_components: update_components)
|
||||
@theme = RemoteTheme.update_zipped_theme(
|
||||
bundle.path,
|
||||
bundle.original_filename,
|
||||
match_theme: match_theme_by_name,
|
||||
user: theme_user,
|
||||
theme_id: theme_id,
|
||||
update_components: update_components
|
||||
)
|
||||
log_theme_change(nil, @theme)
|
||||
render json: @theme, status: :created
|
||||
rescue RemoteTheme::ImportError => e
|
||||
|
||||
@ -119,23 +119,9 @@ class ApplicationController < ActionController::Base
|
||||
|
||||
class RenderEmpty < StandardError; end
|
||||
class PluginDisabled < StandardError; end
|
||||
class EmberCLIHijacked < StandardError; end
|
||||
|
||||
def catch_ember_cli_hijack
|
||||
yield
|
||||
rescue ActionView::Template::Error => ex
|
||||
raise ex unless ex.cause.is_a?(EmberCLIHijacked)
|
||||
send_ember_cli_bootstrap
|
||||
end
|
||||
|
||||
rescue_from RenderEmpty do
|
||||
catch_ember_cli_hijack do
|
||||
with_resolved_locale { render 'default/empty' }
|
||||
end
|
||||
end
|
||||
|
||||
rescue_from EmberCLIHijacked do
|
||||
send_ember_cli_bootstrap
|
||||
with_resolved_locale { render 'default/empty' }
|
||||
end
|
||||
|
||||
rescue_from ArgumentError do |e|
|
||||
@ -324,21 +310,13 @@ class ApplicationController < ActionController::Base
|
||||
rescue Discourse::InvalidAccess
|
||||
return render plain: message, status: status_code
|
||||
end
|
||||
catch_ember_cli_hijack do
|
||||
with_resolved_locale do
|
||||
error_page_opts[:layout] = opts[:include_ember] ? 'application' : 'no_ember'
|
||||
render html: build_not_found_page(error_page_opts)
|
||||
end
|
||||
with_resolved_locale do
|
||||
error_page_opts[:layout] = opts[:include_ember] ? 'application' : 'no_ember'
|
||||
render html: build_not_found_page(error_page_opts)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def send_ember_cli_bootstrap
|
||||
response.headers['X-Discourse-Bootstrap-Required'] = true
|
||||
response.headers['Content-Type'] = "application/json"
|
||||
render json: { preloaded: @preloaded }
|
||||
end
|
||||
|
||||
# If a controller requires a plugin, it will raise an exception if that plugin is
|
||||
# disabled. This allows plugins to be disabled programmatically.
|
||||
def self.requires_plugin(plugin_name)
|
||||
@ -841,7 +819,11 @@ class ApplicationController < ActionController::Base
|
||||
|
||||
if !current_user && SiteSetting.login_required?
|
||||
flash.keep
|
||||
redirect_to_login
|
||||
if (request.format && request.format.json?) || request.xhr? || !request.get?
|
||||
ensure_logged_in
|
||||
else
|
||||
redirect_to_login
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
|
||||
10
app/controllers/associated_groups_controller.rb
Normal file
10
app/controllers/associated_groups_controller.rb
Normal file
@ -0,0 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AssociatedGroupsController < ApplicationController
|
||||
requires_login
|
||||
|
||||
def index
|
||||
guardian.ensure_can_associate_groups!
|
||||
render_serialized(AssociatedGroup.all, AssociatedGroupSerializer, root: 'associated_groups')
|
||||
end
|
||||
end
|
||||
@ -27,12 +27,21 @@ class BootstrapController < ApplicationController
|
||||
add_style(mobile_view? ? :mobile : :desktop)
|
||||
end
|
||||
add_style(:admin) if staff?
|
||||
|
||||
assets_fake_request = ActionDispatch::Request.new(request.env.dup)
|
||||
assets_for_url = params[:for_url]
|
||||
if assets_for_url
|
||||
path, query = assets_for_url.split("?", 2)
|
||||
assets_fake_request.env["PATH_INFO"] = path
|
||||
assets_fake_request.env["QUERY_STRING"] = query
|
||||
end
|
||||
|
||||
Discourse.find_plugin_css_assets(
|
||||
include_official: allow_plugins?,
|
||||
include_unofficial: allow_third_party_plugins?,
|
||||
mobile_view: mobile_view?,
|
||||
desktop_view: !mobile_view?,
|
||||
request: request
|
||||
request: assets_fake_request
|
||||
).each do |file|
|
||||
add_style(file, plugin: true)
|
||||
end
|
||||
@ -49,7 +58,7 @@ class BootstrapController < ApplicationController
|
||||
plugin_js = Discourse.find_plugin_js_assets(
|
||||
include_official: allow_plugins?,
|
||||
include_unofficial: allow_third_party_plugins?,
|
||||
request: request
|
||||
request: assets_fake_request
|
||||
).map { |f| script_asset_path(f) }
|
||||
|
||||
bootstrap = {
|
||||
|
||||
@ -16,8 +16,7 @@ class DraftsController < ApplicationController
|
||||
)
|
||||
|
||||
render json: {
|
||||
drafts: stream ? serialize_data(stream, DraftSerializer) : [],
|
||||
no_results_help: I18n.t("user_activity.no_drafts.self")
|
||||
drafts: stream ? serialize_data(stream, DraftSerializer) : []
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
@ -736,6 +736,10 @@ class GroupsController < ApplicationController
|
||||
end
|
||||
end
|
||||
|
||||
if guardian.can_associate_groups?
|
||||
permitted_params << { associated_group_ids: [] }
|
||||
end
|
||||
|
||||
permitted_params = permitted_params | DiscoursePluginRegistry.group_params
|
||||
|
||||
params.require(:group).permit(*permitted_params)
|
||||
|
||||
@ -134,6 +134,7 @@ class InvitesController < ApplicationController
|
||||
begin
|
||||
invite = Invite.generate(current_user,
|
||||
email: params[:email],
|
||||
domain: params[:domain],
|
||||
skip_email: params[:skip_email],
|
||||
invited_by: current_user,
|
||||
custom_message: params[:custom_message],
|
||||
@ -210,6 +211,17 @@ class InvitesController < ApplicationController
|
||||
Invite.emailed_status_types[:not_required]
|
||||
end
|
||||
end
|
||||
|
||||
invite.domain = nil if invite.email.present?
|
||||
end
|
||||
|
||||
if params.has_key?(:domain)
|
||||
invite.domain = params[:domain]
|
||||
|
||||
if invite.domain.present?
|
||||
invite.email = nil
|
||||
invite.emailed_status = Invite.emailed_status_types[:not_required]
|
||||
end
|
||||
end
|
||||
|
||||
if params[:send_email]
|
||||
|
||||
@ -25,20 +25,7 @@ class UserActionsController < ApplicationController
|
||||
}
|
||||
|
||||
stream = UserAction.stream(opts).to_a
|
||||
if stream.empty? && (help_key = params['no_results_help_key'])
|
||||
if user.id == guardian.user.try(:id)
|
||||
help_key += ".self"
|
||||
else
|
||||
help_key += ".others"
|
||||
end
|
||||
render json: {
|
||||
user_action: [],
|
||||
no_results_help: I18n.t(help_key)
|
||||
}
|
||||
else
|
||||
render_serialized(stream, UserActionSerializer, root: 'user_actions')
|
||||
end
|
||||
|
||||
render_serialized(stream, UserActionSerializer, root: 'user_actions')
|
||||
end
|
||||
|
||||
def show
|
||||
|
||||
@ -171,6 +171,7 @@ class Users::OmniauthCallbacksController < ApplicationController
|
||||
elsif Guardian.new(user).can_access_forum? && user.active # log on any account that is active with forum access
|
||||
begin
|
||||
user.save! if @auth_result.apply_user_attributes!
|
||||
@auth_result.apply_associated_attributes!
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
@auth_result.failed = true
|
||||
@auth_result.failed_reason = e.record.errors.full_messages.join(", ")
|
||||
|
||||
@ -928,6 +928,8 @@ class UsersController < ApplicationController
|
||||
RateLimiter.new(nil, "email-login-min-#{user.id}", 3, 1.minute).performed!
|
||||
|
||||
if user_presence
|
||||
DiscourseEvent.trigger(:before_email_login, user)
|
||||
|
||||
email_token = user.email_tokens.create!(email: user.email, scope: EmailToken.scopes[:email_login])
|
||||
|
||||
Jobs.enqueue(:critical_user_email,
|
||||
@ -1619,10 +1621,7 @@ class UsersController < ApplicationController
|
||||
|
||||
if bookmark_list.bookmarks.empty?
|
||||
render json: {
|
||||
bookmarks: [],
|
||||
no_results_help: I18n.t(
|
||||
params[:q].present? ? "user_activity.no_bookmarks.search" : "user_activity.no_bookmarks.self"
|
||||
)
|
||||
bookmarks: []
|
||||
}
|
||||
else
|
||||
page = params[:page].to_i + 1
|
||||
|
||||
@ -13,7 +13,7 @@ class WebhooksController < ActionController::Base
|
||||
def sendgrid
|
||||
events = params["_json"] || [params]
|
||||
events.each do |event|
|
||||
message_id = Email.message_id_clean((event["smtp-id"] || ""))
|
||||
message_id = Email::MessageIdService.message_id_clean((event["smtp-id"] || ""))
|
||||
to_address = event["email"]
|
||||
if event["event"] == "bounce"
|
||||
if event["status"]["4."]
|
||||
@ -150,7 +150,7 @@ class WebhooksController < ActionController::Base
|
||||
return mailgun_failure unless valid_mailgun_signature?(params["token"], params["timestamp"], params["signature"])
|
||||
|
||||
event = params["event"]
|
||||
message_id = Email.message_id_clean(params["Message-Id"])
|
||||
message_id = Email::MessageIdService.message_id_clean(params["Message-Id"])
|
||||
to_address = params["recipient"]
|
||||
|
||||
# only handle soft bounces, because hard bounces are also handled
|
||||
|
||||
@ -647,10 +647,4 @@ module ApplicationHelper
|
||||
current_user ? nil : value
|
||||
end
|
||||
end
|
||||
|
||||
def hijack_if_ember_cli!
|
||||
if request.headers["HTTP_X_DISCOURSE_EMBER_CLI"] == "true"
|
||||
raise ApplicationController::EmberCLIHijacked.new
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@ -1,11 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Jobs
|
||||
class MigrateNormalizedEmails < ::Jobs::Onceoff
|
||||
def execute_onceoff(args)
|
||||
::UserEmail.find_each do |user_email|
|
||||
user_email.update(normalized_email: user_email.normalize_email)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -111,7 +111,8 @@ module Jobs
|
||||
email = invite[:email]
|
||||
groups = get_groups(invite[:groups])
|
||||
topic = get_topic(invite[:topic_id])
|
||||
user_fields = get_user_fields(invite.except(:email, :groups, :topic_id))
|
||||
locale = invite[:locale]
|
||||
user_fields = get_user_fields(invite.except(:email, :groups, :topic_id, :locale))
|
||||
|
||||
begin
|
||||
if user = Invite.find_user_by_email(email)
|
||||
@ -133,13 +134,26 @@ module Jobs
|
||||
end
|
||||
user.save_custom_fields
|
||||
end
|
||||
|
||||
if locale.present?
|
||||
user.locale = locale
|
||||
user.save!
|
||||
end
|
||||
else
|
||||
if user_fields.present?
|
||||
if user_fields.present? || locale.present?
|
||||
user = User.where(staged: true).find_by_email(email)
|
||||
user ||= User.new(username: UserNameSuggester.suggest(email), email: email, staged: true)
|
||||
user_fields.each do |user_field, value|
|
||||
user.set_user_field(user_field, value)
|
||||
|
||||
if user_fields.present?
|
||||
user_fields.each do |user_field, value|
|
||||
user.set_user_field(user_field, value)
|
||||
end
|
||||
end
|
||||
|
||||
if locale.present?
|
||||
user.locale = locale
|
||||
end
|
||||
|
||||
user.save!
|
||||
end
|
||||
|
||||
|
||||
11
app/jobs/scheduled/clean_up_associated_groups.rb
Normal file
11
app/jobs/scheduled/clean_up_associated_groups.rb
Normal file
@ -0,0 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Jobs
|
||||
class CleanUpAssociatedGroups < ::Jobs::Scheduled
|
||||
every 1.day
|
||||
|
||||
def execute(args)
|
||||
AssociatedGroup.cleanup!
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -39,9 +39,13 @@ module Jobs
|
||||
next if ReviewableQueuedPost.pending.where("payload->>'raw' LIKE '%#{upload.sha1}%' OR payload->>'raw' LIKE '%#{encoded_sha}%'").exists?
|
||||
next if Draft.where("data LIKE '%#{upload.sha1}%' OR data LIKE '%#{encoded_sha}%'").exists?
|
||||
next if UserProfile.where("bio_raw LIKE '%#{upload.sha1}%' OR bio_raw LIKE '%#{encoded_sha}%'").exists?
|
||||
if defined?(ChatMessage) &&
|
||||
ChatMessage.where("message LIKE ? OR message LIKE ?", "%#{upload.sha1}%", "%#{encoded_sha}%").exists?
|
||||
next
|
||||
if defined?(ChatMessage)
|
||||
# TODO after May 2022 - remove this. No longer needed as chat uploads are in a table
|
||||
next if ChatMessage.where("message LIKE ? OR message LIKE ?", "%#{upload.sha1}%", "%#{encoded_sha}%").exists?
|
||||
end
|
||||
|
||||
if defined?(ChatUpload)
|
||||
next if ChatUpload.where(upload: upload).exists?
|
||||
end
|
||||
upload.destroy
|
||||
else
|
||||
|
||||
@ -22,14 +22,19 @@ module Jobs
|
||||
usernames = active_moderator_usernames
|
||||
mentions = usernames.size > 0 ? "@#{usernames.join(', @')} " : ""
|
||||
|
||||
@sent_reminder = PostCreator.create(
|
||||
Discourse.system_user,
|
||||
target_group_names: Group[:moderators].name,
|
||||
archetype: Archetype.private_message,
|
||||
subtype: TopicSubtype.system_message,
|
||||
title: I18n.t('reviewables_reminder.subject_template', count: reviewable_ids.size),
|
||||
raw: mentions + I18n.t('reviewables_reminder.submitted', count: SiteSetting.notify_about_flags_after, base_path: Discourse.base_path)
|
||||
).present?
|
||||
message = GroupMessage.new(
|
||||
Group[:moderators].name,
|
||||
'reviewables_reminder',
|
||||
{
|
||||
limit_once_per: false,
|
||||
message_params: { mentions: mentions, count: SiteSetting.notify_about_flags_after }
|
||||
}
|
||||
)
|
||||
|
||||
Topic.transaction do
|
||||
message.delete_previous!(match_raw: false)
|
||||
@sent_reminder = message.create.present?
|
||||
end
|
||||
|
||||
self.class.last_notified_id = reviewable_ids[0]
|
||||
end
|
||||
|
||||
@ -51,6 +51,15 @@ class ApiKeyScope < ActiveRecord::Base
|
||||
},
|
||||
email: {
|
||||
receive_emails: { actions: %w[admin/email#handle_mail] }
|
||||
},
|
||||
badges: {
|
||||
create: { actions: %w[admin/badges#create] },
|
||||
show: { actions: %w[badges#show] },
|
||||
update: { actions: %w[admin/badges#update] },
|
||||
delete: { actions: %w[admin/badges#destroy] },
|
||||
list_user_badges: { actions: %w[user_badges#username], params: %i[username] },
|
||||
assign_badge_to_user: { actions: %w[user_badges#create], params: %i[username] },
|
||||
revoke_badge_from_user: { actions: %w[user_badges#destroy] },
|
||||
}
|
||||
}
|
||||
|
||||
@ -79,32 +88,29 @@ class ApiKeyScope < ActiveRecord::Base
|
||||
end
|
||||
|
||||
def find_urls(actions:, methods:)
|
||||
action_urls = []
|
||||
method_urls = []
|
||||
urls = []
|
||||
|
||||
if actions.present?
|
||||
Rails.application.routes.routes.reduce([]) do |memo, route|
|
||||
Rails.application.routes.routes.each do |route|
|
||||
defaults = route.defaults
|
||||
action = "#{defaults[:controller].to_s}##{defaults[:action]}"
|
||||
path = route.path.spec.to_s.gsub(/\(\.:format\)/, '')
|
||||
api_supported_path = path.end_with?('.rss') || route.path.requirements[:format]&.match?('json')
|
||||
excluded_paths = %w[/new-topic /new-message /exception]
|
||||
|
||||
memo.tap do |m|
|
||||
if actions.include?(action) && api_supported_path && !excluded_paths.include?(path)
|
||||
m << "#{path} (#{route.verb})"
|
||||
end
|
||||
if actions.include?(action) && api_supported_path && !excluded_paths.include?(path)
|
||||
urls << "#{path} (#{route.verb})"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if methods.present?
|
||||
methods.each do |method|
|
||||
method_urls << "* (#{method})"
|
||||
urls << "* (#{method})"
|
||||
end
|
||||
end
|
||||
|
||||
action_urls + method_urls
|
||||
urls
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
38
app/models/associated_group.rb
Normal file
38
app/models/associated_group.rb
Normal file
@ -0,0 +1,38 @@
|
||||
# frozen_string_literal: true
|
||||
class AssociatedGroup < ActiveRecord::Base
|
||||
has_many :user_associated_groups, dependent: :destroy
|
||||
has_many :users, through: :user_associated_groups
|
||||
has_many :group_associated_groups, dependent: :destroy
|
||||
has_many :groups, through: :group_associated_groups
|
||||
|
||||
def label
|
||||
"#{provider_name}:#{name}"
|
||||
end
|
||||
|
||||
def self.has_provider?
|
||||
Discourse.enabled_authenticators.any? { |a| a.provides_groups? }
|
||||
end
|
||||
|
||||
def self.cleanup!
|
||||
AssociatedGroup.left_joins(:group_associated_groups, :user_associated_groups)
|
||||
.where("group_associated_groups.id IS NULL AND user_associated_groups.id IS NULL")
|
||||
.where("last_used < ?", 1.week.ago).delete_all
|
||||
end
|
||||
end
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: associated_groups
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# name :string not null
|
||||
# provider_name :string not null
|
||||
# provider_id :string not null
|
||||
# last_used :datetime not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# associated_groups_provider_id (provider_name,provider_id) UNIQUE
|
||||
#
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user