Merge remote-tracking branch 'origin/main' into generic-import

This commit is contained in:
Gerhard Schlager 2021-12-13 12:52:57 +01:00
commit c0ad2adeef
336 changed files with 5789 additions and 1507 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -26,5 +26,3 @@
<div class="admin-detail pull-left mobile-closed">
{{outlet}}
</div>
<div class="clearfix"></div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,6 +10,9 @@
post=model.post
whisper=model.whisper
noBump=model.noBump
options=(hash
mobilePlacementStrategy="fixed"
)
}}
<span class="action-title">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
<section>{{yield}}</section>

View File

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

View File

@ -56,6 +56,7 @@
{{#if canLoginLocal}}
{{#unless showSecurityKey }}
{{d-button action=(action "login")
id="login-button"
icon="unlock"
label=loginButtonLabel
disabled=loginDisabled

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -56,6 +56,10 @@ export default Component.extend(UtilsMixin, {
}
},
mouseDown() {
return false;
},
click(event) {
event.preventDefault();
event.stopPropagation();

View File

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

View File

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

View File

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

View File

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

View File

@ -372,7 +372,7 @@ table {
}
.profiler-results.profiler-left {
top: 60px !important;
top: var(--header-offset) !important;
}
.flex-center-align {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -21,4 +21,7 @@
.fps-result {
padding-left: 0;
padding-right: 0;
.search-link .topic-statuses {
font-size: 1.1em;
}
}

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

View File

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

View File

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

View 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