Merge branch 'main' into generic-import

This commit is contained in:
Gerhard Schlager 2021-11-11 15:24:06 +01:00
commit 1c31741a0b
133 changed files with 1149 additions and 615 deletions

View File

@ -254,7 +254,7 @@ GEM
racc (~> 1.4)
nokogiri (1.12.5-x86_64-linux)
racc (~> 1.4)
oauth (0.5.7)
oauth (0.5.8)
oauth2 (1.4.7)
faraday (>= 0.8, < 2.0)
jwt (>= 1.0, < 3.0)

View File

@ -48,7 +48,7 @@ export default Component.extend({
}
// update the formatted logs & cache index
this.setProperties({
formattedLogs: formattedLogs,
formattedLogs,
index: logs.length,
});
// force rerender

View File

@ -24,7 +24,7 @@ export default Component.extend({
const config = {
type: this.type,
data: data,
data,
options: {
responsive: true,
plugins: {

View File

@ -63,7 +63,7 @@ export default Component.extend({
},
_addValue(value, secret) {
this.collection.addObject({ key: value, secret: secret });
this.collection.addObject({ key: value, secret });
this._saveValues();
},

View File

@ -10,7 +10,8 @@ import { ajax } from "discourse/lib/ajax";
export default Controller.extend({
userModes: null,
useGlobalKey: false,
scopeModes: null,
globalScopes: null,
scopes: null,
init() {
@ -20,6 +21,13 @@ export default Controller.extend({
{ id: "all", name: I18n.t("admin.api.all_users") },
{ id: "single", name: I18n.t("admin.api.single_user") },
]);
this.set("scopeModes", [
{ id: "granular", name: I18n.t("admin.api.scopes.granular") },
{ id: "read_only", name: I18n.t("admin.api.scopes.read_only") },
{ id: "global", name: I18n.t("admin.api.scopes.global") },
]);
this._loadScopes();
},
@ -49,14 +57,23 @@ export default Controller.extend({
this.set("userMode", userMode);
},
@action
changeScopeMode(scopeMode) {
this.set("scopeMode", scopeMode);
},
@action
save() {
if (!this.useGlobalKey) {
if (this.scopeMode === "granular") {
const selectedScopes = Object.values(this.scopes)
.flat()
.filterBy("selected");
this.model.set("scopes", selectedScopes);
} else if (this.scopeMode === "read_only") {
this.model.set("scopes", [this.globalScopes.findBy("key", "read")]);
} else if (this.scopeMode === "all") {
this.model.set("scopes", null);
}
return this.model.save().catch(popupAjaxError);
@ -78,6 +95,10 @@ export default Controller.extend({
_loadScopes() {
return ajax("/admin/api/keys/scopes.json")
.then((data) => {
// remove global scopes because there is a different dropdown
this.set("globalScopes", data.scopes.global);
delete data.scopes.global;
this.set("scopes", data.scopes);
})
.catch(popupAjaxError);

View File

@ -12,6 +12,9 @@ export default Controller.extend({
uploadLabel: i18n("admin.backups.upload.label"),
backupLocation: setting("backup_location"),
localBackupStorage: equal("backupLocation", "local"),
enableExperimentalBackupUploader: setting(
"enable_experimental_backup_uploader"
),
@discourseComputed("status.allowRestore", "status.isOperationRunning")
restoreTitle(allowRestore, isOperationRunning) {

View File

@ -121,7 +121,7 @@ export default Controller.extend({
},
filterBySubject(subject) {
this.changeFilters({ subject: subject });
this.changeFilters({ subject });
},
exportStaffActionLogs() {

View File

@ -49,7 +49,7 @@ export default Controller.extend(GrantBadgeController, {
let result = {
badge: badges[0].badge,
granted_at: lastGranted,
badges: badges,
badges,
count: badges.length,
grouped: true,
};

View File

@ -592,7 +592,7 @@ export default Controller.extend(CanCheckEmails, {
(deletedPosts * 100) / user.get("post_count")
);
progressModal.setProperties({
deletedPercentage: deletedPercentage,
deletedPercentage,
});
performDelete(progressModal);
}

View File

@ -35,11 +35,11 @@ const Permalink = EmberObject.extend({
Permalink.reopenClass({
findAll: function (filter) {
return ajax("/admin/permalinks.json", { data: { filter: filter } }).then(
function (permalinks) {
return permalinks.map((p) => Permalink.create(p));
}
);
return ajax("/admin/permalinks.json", { data: { filter } }).then(function (
permalinks
) {
return permalinks.map((p) => Permalink.create(p));
});
},
});

View File

@ -672,7 +672,7 @@ Report.reopenClass({
Report.fillMissingDates(json.report);
}
const model = Report.create({ type: type });
const model = Report.create({ type });
model.setProperties(json.report);
if (json.report.related_report) {

View File

@ -42,7 +42,7 @@ const ScreenedIpAddress = EmberObject.extend({
ScreenedIpAddress.reopenClass({
findAll(filter) {
return ajax("/admin/logs/screened_ip_addresses.json", {
data: { filter: filter },
data: { filter },
}).then((screened_ips) =>
screened_ips.map((b) => ScreenedIpAddress.create(b))
);

View File

@ -44,7 +44,7 @@ export default RestModel.extend({
},
groupFinder(term) {
return Group.findAll({ term: term, ignore_automatic: false });
return Group.findAll({ term, ignore_automatic: false });
},
@discourseComputed("wildcard_web_hook", "web_hook_event_types.[]")

View File

@ -32,7 +32,7 @@ export default DiscourseRoute.extend({
});
controller.setProperties({
badgeGroupings: badgeGroupings,
badgeGroupings,
badgeTypes: json.badge_types,
protectedSystemFields: json.admin_badges.protected_system_fields,
badgeTriggers,

View File

@ -39,8 +39,8 @@ export default Route.extend({
});
controller.setProperties({
model: model,
parentController: parentController,
model,
parentController,
allThemes: parentController.get("model"),
colorSchemeId: model.get("color_scheme_id"),
colorSchemes: parentController.get("model.extras.color_schemes"),

View File

@ -24,7 +24,7 @@ export default Route.extend({
controller.setProperties({
siteText,
saved: false,
localeFullName: localeFullName,
localeFullName,
});
},
});

View File

@ -36,12 +36,18 @@
{{/admin-form-row}}
{{/if}}
{{#admin-form-row label="admin.api.use_global_key"}}
{{input type="checkbox" checked=useGlobalKey}}
{{#admin-form-row label="admin.api.scope_mode"}}
{{combo-box content=scopeModes value=scopeMode onChange=(action "changeScopeMode")}}
{{#if (eq scopeMode "read_only")}}
<p>{{i18n "admin.api.scopes.descriptions.global.read"}}</p>
{{else if (eq scopeMode "global")}}
<p>{{i18n "admin.api.scopes.global_description"}}</p>
{{/if}}
{{/admin-form-row}}
{{#unless useGlobalKey}}
<div class="scopes-title">{{i18n "admin.api.scopes.title"}}</div>
{{#if (eq scopeMode "granular")}}
<h2 class="scopes-title">{{i18n "admin.api.scopes.title"}}</h2>
<p>{{i18n "admin.api.scopes.description"}}</p>
<table class="scopes-table grid">
<thead>
@ -82,7 +88,7 @@
{{/each-in}}
</tbody>
</table>
{{/unless}}
{{/if}}
{{d-button icon="check" label="admin.api.save" action=(action "save") class="btn-primary" disabled=saveDisabled}}
{{/if}}

View File

@ -83,7 +83,7 @@
{{/admin-form-row}}
{{#if model.api_key_scopes.length}}
<div class="scopes-title">{{i18n "admin.api.scopes.title"}}</div>
<h2 class="scopes-title">{{i18n "admin.api.scopes.title"}}</h2>
<table class="scopes-table grid">
<thead>

View File

@ -8,7 +8,11 @@
title="admin.backups.upload.title"
class="btn-default"}}
{{else}}
{{backup-uploader done=(route-action "remoteUploadSuccess")}}
{{#if enableExperimentalBackupUploader}}
{{uppy-backup-uploader done=(route-action "remoteUploadSuccess")}}
{{else}}
{{backup-uploader done=(route-action "remoteUploadSuccess")}}
{{/if}}
{{/if}}
{{#if site.isReadOnly}}

View File

@ -6,6 +6,7 @@
{{emoji-uploader
emojiGroups=emojiGroups
done=(action "emojiUploaded")
id="emoji-uploader"
}}
<hr>

View File

@ -9,7 +9,11 @@
icon="download"
label="admin.watched_words.download"}}
{{watched-word-uploader uploading=uploading actionKey=actionNameKey done=(action "uploadComplete")}}
{{watched-word-uploader
id="watched-word-uploader"
uploading=uploading
actionKey=actionNameKey
done=(action "uploadComplete")}}
{{d-button
class="watched-word-test"

View File

@ -550,7 +550,7 @@ export default Component.extend(ComposerUpload, {
if (found.indexOf(name) === -1) {
this.groupsMentioned([
{
name: name,
name,
user_count: $e.data("mentionable-user-count"),
max_mentions: $e.data("max-mentions"),
},

View File

@ -83,7 +83,7 @@ export default Component.extend({
!topic.archived &&
!topic.closed &&
!topic.deleted,
topic: topic,
topic,
});
},

View File

@ -10,7 +10,7 @@ export default Component.extend({
this._super(...arguments);
this._modalAlertElement = document.getElementById("modal-alert");
if (this._modalAlertElement) {
this._modalAlertElement.innerHTML = "";
this._clearFlash();
}
let fixedParent = $(this.element).closest(".d-modal.fixed-modal");

View File

@ -77,7 +77,12 @@ export default MountWidget.extend({
if (this.isDestroyed || this.isDestroying) {
return;
}
if (isWorkaroundActive()) {
if (
isWorkaroundActive() ||
document.webkitFullscreenElement ||
document.fullscreenElement
) {
return;
}

View File

@ -37,12 +37,12 @@ export default Component.extend({
const inboxFilter = suggestedGroupName ? "group" : "user";
const unreadCount = this.pmTopicTrackingState.lookupCount("unread", {
inboxFilter: inboxFilter,
inboxFilter,
groupName: suggestedGroupName,
});
const newCount = this.pmTopicTrackingState.lookupCount("new", {
inboxFilter: inboxFilter,
inboxFilter,
groupName: suggestedGroupName,
});
@ -54,7 +54,7 @@ export default Component.extend({
BOTH: hasBoth,
UNREAD: unreadCount,
NEW: newCount,
username: username,
username,
groupName: suggestedGroupName,
groupLink: this._groupLink(username, suggestedGroupName),
basePath: getURL(""),

View File

@ -0,0 +1,25 @@
import Component from "@ember/component";
import I18n from "I18n";
import UppyUploadMixin from "discourse/mixins/uppy-upload";
import discourseComputed from "discourse-common/utils/decorators";
export default Component.extend(UppyUploadMixin, {
tagName: "span",
type: "backup",
useMultipartUploadsIfAvailable: true,
@discourseComputed("uploading", "uploadProgress")
uploadButtonText(uploading, progress) {
return uploading
? I18n.t("admin.backups.upload.uploading_progress", { progress })
: I18n.t("admin.backups.upload.label");
},
validateUploadedFilesOptions() {
return { skipValidation: true };
},
uploadDone() {
this.done();
},
});

View File

@ -50,7 +50,7 @@ export default Controller.extend(ModalFunctionality, {
);
},
() => {
this.flash(I18n.t("topic.change_owner.error"), "alert-error");
this.flash(I18n.t("topic.change_owner.error"), "error");
this.set("saving", false);
}
);

View File

@ -54,7 +54,7 @@ export default Controller.extend(ModalFunctionality, {
next(() => DiscourseURL.routeTo(topic.url));
})
.catch(() =>
this.flash(I18n.t("topic.change_timestamp.error"), "alert-error")
this.flash(I18n.t("topic.change_timestamp.error"), "error")
)
.finally(() => this.set("saving", false));

View File

@ -29,7 +29,7 @@ export default Controller.extend(ModalFunctionality, {
.destroy(this.currentUser)
.then(() => this.send("closeModal"))
.catch(() => {
this.flash(I18n.t("post.controls.delete_topic_error"), "alert-error");
this.flash(I18n.t("post.controls.delete_topic_error"), "error");
this.set("deletingTopic", false);
});

View File

@ -185,7 +185,7 @@ export default Controller.extend(ModalFunctionality, {
) {
this.flash(
I18n.t("topic.topic_status_update.time_frame_required"),
"alert-error"
"error"
);
return;
}
@ -195,19 +195,13 @@ export default Controller.extend(ModalFunctionality, {
!this.get("topicTimer.updateTime")
) {
if (this.get("topicTimer.duration_minutes") <= 0) {
this.flash(
I18n.t("topic.topic_status_update.min_duration"),
"alert-error"
);
this.flash(I18n.t("topic.topic_status_update.min_duration"), "error");
return;
}
// cannot be more than 20 years
if (this.get("topicTimer.duration_minutes") > 20 * 365 * 1440) {
this.flash(
I18n.t("topic.topic_status_update.max_duration"),
"alert-error"
);
this.flash(I18n.t("topic.topic_status_update.max_duration"), "error");
return;
}
}

View File

@ -37,7 +37,7 @@ export default Controller.extend({
ajax({
url: `/session/email-login/${this.model.token}`,
type: "POST",
data: data,
data,
})
.then((result) => {
if (result.success) {

View File

@ -48,8 +48,8 @@ export default Controller.extend(ModalFunctionality, GrantBadgeController, {
UserBadge.findByUsername(this.get("post.username")),
]).then(([allBadges, userBadges]) => {
this.setProperties({
allBadges: allBadges,
userBadges: userBadges,
allBadges,
userBadges,
loading: false,
});
});

View File

@ -13,7 +13,7 @@ export default Controller.extend(ModalFunctionality, {
if (!this.ignoredUntil || !this.ignoredUsername) {
this.flash(
I18n.t("user.user_notifications.ignore_duration_time_frame_required"),
"alert-error"
"error"
);
return;
}

View File

@ -11,7 +11,7 @@ export default Controller.extend(ModalFunctionality, {
if (!this.ignoredUntil) {
this.flash(
I18n.t("user.user_notifications.ignore_duration_time_frame_required"),
"alert-error"
"error"
);
return;
}

View File

@ -83,7 +83,7 @@ export default Controller.extend(
@discourseComputed("email")
yourEmailMessage(email) {
return I18n.t("invites.your_email", { email: email });
return I18n.t("invites.your_email", { email });
},
@discourseComputed

View File

@ -273,9 +273,7 @@ export default Controller.extend(ModalFunctionality, {
}
this.set("loggingIn", true);
loginMethod
.doLogin({ signup: signup })
.catch(() => this.set("loggingIn", false));
loginMethod.doLogin({ signup }).catch(() => this.set("loggingIn", false));
},
createAccount() {

View File

@ -121,7 +121,7 @@ export default Controller.extend(PasswordValidation, {
this.setProperties({
securityKeyRequired: true,
password: null,
errorMessage: errorMessage,
errorMessage,
});
}
);

View File

@ -73,7 +73,7 @@ export default Controller.extend(ModalFunctionality, {
name: this.model.username_lower,
},
pubKeyCredParams: this.supported_algorithms.map((alg) => {
return { type: "public-key", alg: alg };
return { type: "public-key", alg };
}),
excludeCredentials: this.existing_active_credential_ids.map(
(credentialId) => {

View File

@ -103,7 +103,17 @@ export default Controller.extend(bufferedProperty("model"), {
),
updateQueryParams() {
this.setProperties(this.get("model.postStream.streamFilters"));
const filters = this.get("model.postStream.streamFilters");
if (Object.keys(filters).length > 0) {
this.setProperties(filters);
} else {
this.setProperties({
username_filters: null,
filter: null,
replies_to_post_number: null,
});
}
},
@observes("model.title", "category")
@ -1598,6 +1608,12 @@ export default Controller.extend(bufferedProperty("model"), {
.then(() => refresh({ id: data.id, refreshLikes: true }));
break;
}
case "liked": {
postStream
.triggerLikedPost(data.id, data.likes_count)
.then(() => refresh({ id: data.id, refreshLikes: true }));
break;
}
case "revised":
case "rebaked": {
postStream

View File

@ -60,7 +60,7 @@ export default Controller.extend(BulkTopicSelection, {
const opts = {
inbox: this.inbox,
topicIds: topicIds,
topicIds,
};
if (this.group) {

View File

@ -25,9 +25,9 @@ registerUnbound("format-date", function (val, params) {
let date = new Date(val);
return htmlSafe(
autoUpdatingRelativeAge(date, {
format: format,
title: title,
leaveAgo: leaveAgo,
format,
title,
leaveAgo,
})
);
}

View File

@ -72,7 +72,7 @@ function renderAvatar(user, options) {
size: options.imageSize,
extraClasses: get(user, "extras") || options.extraClasses,
title: title || displayName,
avatarTemplate: avatarTemplate,
avatarTemplate,
});
} else {
return "";

View File

@ -110,7 +110,7 @@ export function ajax() {
});
if (args.returnXHR) {
data = { result: data, xhr: xhr };
data = { result: data, xhr };
}
run(null, resolve, data);
@ -145,8 +145,8 @@ export function ajax() {
run(null, reject, {
jqXHR: xhr,
textStatus: textStatus,
errorThrown: errorThrown,
textStatus,
errorThrown,
});
};

View File

@ -32,7 +32,7 @@ function searchTags(term, categories, limit) {
function () {
oldSearch = $.ajax(getURL("/tags/filter/search"), {
type: "GET",
data: { limit: limit, q },
data: { limit, q },
});
let returnVal = CANCELLED_STATUS;

View File

@ -1564,6 +1564,9 @@ function getPluginApi(version) {
owner.registry.register("plugin-api:main", pluginApi, {
instantiate: false,
});
} else {
// If we are re-using an instance, make sure the container is correct
pluginApi.container = owner;
}
// We are recycling the compatible object, but let's update to the higher version

View File

@ -27,10 +27,10 @@ export function extractLinkMeta(topic) {
}
const meta = {
target: target,
target,
href,
domain: domain,
rel: rel,
domain,
rel,
};
if (_decorators.length) {

View File

@ -99,9 +99,6 @@ function positioningWorkaround($fixedElement) {
positioningWorkaround.blur = function (evt) {
if (workaroundActive) {
document.body.classList.remove("ios-safari-composer-hacks");
if (caps.isIOS15Safari) {
document.body.classList.remove("ios-safari-15-hack");
}
window.scrollTo(0, originalScrollTop);
if (evt && evt.target) {
@ -192,9 +189,6 @@ function positioningWorkaround($fixedElement) {
}
document.body.classList.add("ios-safari-composer-hacks");
if (caps.isIOS15Safari) {
document.body.classList.add("ios-safari-15-hack");
}
window.scrollTo(0, 0);
if (!iOSWithVisualViewport()) {

View File

@ -147,7 +147,7 @@ export default class {
const { timings, topicTime, topicId } = this._consolidatedTimings.pop();
const data = {
timings: timings,
timings,
topic_time: topicTime,
topic_id: topicId,
};

View File

@ -143,7 +143,7 @@ export function searchForTerm(term, opts) {
}
// Only include the data we have
const data = { term: term };
const data = { term };
if (opts.typeFilter) {
data.type_filter = opts.typeFilter;
}

View File

@ -55,7 +55,7 @@ function performSearch(
}
let data = {
term: term,
term,
topic_id: topicId,
category_id: categoryId,
include_groups: includeGroups,
@ -65,7 +65,7 @@ function performSearch(
topic_allowed_users: allowedUsers,
include_staged_users: includeStagedUsers,
last_seen_users: lastSeenUsers,
limit: limit,
limit,
};
if (customUserSearchOptions) {

View File

@ -99,9 +99,7 @@ export function avatarImg(options, customGetURL) {
}
export function tinyAvatar(avatarTemplate, options) {
return avatarImg(
deepMerge({ avatarTemplate: avatarTemplate, size: "tiny" }, options)
);
return avatarImg(deepMerge({ avatarTemplate, size: "tiny" }, options));
}
export function postUrl(slug, topicId, postNumber) {
@ -219,7 +217,7 @@ export function caretRowCol(el) {
return sum + row.length + 1;
}, 0);
return { rowNum: rowNum, colNum: colNum };
return { rowNum, colNum };
}
// Determine the position of the caret in an element

View File

@ -38,7 +38,7 @@ export function getWebauthnCredential(
.get({
publicKey: {
challenge: challengeBuffer,
allowCredentials: allowCredentials,
allowCredentials,
timeout: 60000,
// see https://chromium.googlesource.com/chromium/src/+/master/content/browser/webauth/uv_preferred.md for why

View File

@ -1,13 +1,11 @@
import Mixin from "@ember/object/mixin";
import { Promise } from "rsvp";
import ExtendableUploader from "discourse/mixins/extendable-uploader";
import { ajax } from "discourse/lib/ajax";
import UppyS3Multipart from "discourse/mixins/uppy-s3-multipart";
import { deepMerge } from "discourse-common/lib/object";
import UppyChecksum from "discourse/lib/uppy-checksum-plugin";
import Uppy from "@uppy/core";
import DropTarget from "@uppy/drop-target";
import XHRUpload from "@uppy/xhr-upload";
import AwsS3Multipart from "@uppy/aws-s3-multipart";
import { warn } from "@ember/debug";
import I18n from "I18n";
import getURL from "discourse-common/lib/get-url";
@ -33,7 +31,7 @@ import { cacheShortUploadUrl } from "pretty-text/upload-short-url";
// and the most important _bindUploadTarget which handles all the main upload
// functionality and event binding.
//
export default Mixin.create(ExtendableUploader, {
export default Mixin.create(ExtendableUploader, UppyS3Multipart, {
@observes("composerModel.uploadCancelled")
_cancelUpload() {
if (!this.get("composerModel.uploadCancelled")) {
@ -390,158 +388,6 @@ export default Mixin.create(ExtendableUploader, {
});
},
_useS3MultipartUploads() {
const self = this;
const retryDelays = [0, 1000, 3000, 5000];
this._uppyInstance.use(AwsS3Multipart, {
// controls how many simultaneous _chunks_ are uploaded, not files,
// which in turn controls the minimum number of chunks presigned
// in each batch (limit / 2)
//
// the default, and minimum, chunk size is 5mb. we can control the
// chunk size via getChunkSize(file), so we may want to increase
// the chunk size for larger files
limit: 10,
retryDelays,
createMultipartUpload(file) {
self._uppyInstance.emit("create-multipart", file.id);
const data = {
file_name: file.name,
file_size: file.size,
upload_type: file.meta.upload_type,
metadata: file.meta,
};
// the sha1 checksum is set by the UppyChecksum plugin, except
// for in cases where the browser does not support the required
// crypto mechanisms or an error occurs. it is an additional layer
// of security, and not required.
if (file.meta.sha1_checksum) {
data.metadata = { "sha1-checksum": file.meta.sha1_checksum };
}
return ajax("/uploads/create-multipart.json", {
type: "POST",
data,
// uppy is inconsistent, an error here fires the upload-error event
}).then((responseData) => {
self._uppyInstance.emit("create-multipart-success", file.id);
file.meta.unique_identifier = responseData.unique_identifier;
return {
uploadId: responseData.external_upload_identifier,
key: responseData.key,
};
});
},
prepareUploadParts(file, partData) {
if (file.preparePartsRetryAttempts === undefined) {
file.preparePartsRetryAttempts = 0;
}
return ajax("/uploads/batch-presign-multipart-parts.json", {
type: "POST",
data: {
part_numbers: partData.partNumbers,
unique_identifier: file.meta.unique_identifier,
},
})
.then((data) => {
if (file.preparePartsRetryAttempts) {
delete file.preparePartsRetryAttempts;
self._consoleDebug(
`[uppy] Retrying batch fetch for ${file.id} was successful, continuing.`
);
}
return { presignedUrls: data.presigned_urls };
})
.catch((err) => {
const status = err.jqXHR.status;
// it is kind of ugly to have to track the retry attempts for
// the file based on the retry delays, but uppy's `retryable`
// function expects the rejected Promise data to be structured
// _just so_, and provides no interface for us to tell how many
// times the upload has been retried (which it tracks internally)
//
// if we exceed the attempts then there is no way that uppy will
// retry the upload once again, so in that case the alert can
// be safely shown to the user that their upload has failed.
if (file.preparePartsRetryAttempts < retryDelays.length) {
file.preparePartsRetryAttempts += 1;
const attemptsLeft =
retryDelays.length - file.preparePartsRetryAttempts + 1;
self._consoleDebug(
`[uppy] Fetching a batch of upload part URLs for ${file.id} failed with status ${status}, retrying ${attemptsLeft} more times...`
);
return Promise.reject({ source: { status } });
} else {
self._consoleDebug(
`[uppy] Fetching a batch of upload part URLs for ${file.id} failed too many times, throwing error.`
);
// uppy is inconsistent, an error here does not fire the upload-error event
self._handleUploadError(file, err);
}
});
},
completeMultipartUpload(file, data) {
self._uppyInstance.emit("complete-multipart", file.id);
const parts = data.parts.map((part) => {
return { part_number: part.PartNumber, etag: part.ETag };
});
return ajax("/uploads/complete-multipart.json", {
type: "POST",
contentType: "application/json",
data: JSON.stringify({
parts,
unique_identifier: file.meta.unique_identifier,
pasted: file.meta.pasted,
for_private_message: file.meta.for_private_message,
}),
// uppy is inconsistent, an error here fires the upload-error event
}).then((responseData) => {
self._uppyInstance.emit("complete-multipart-success", file.id);
return responseData;
});
},
abortMultipartUpload(file, { key, uploadId }) {
// if the user cancels the upload before the key and uploadId
// are stored from the createMultipartUpload response then they
// will not be set, and we don't have to abort the upload because
// it will not exist yet
if (!key || !uploadId) {
return;
}
// this gives us a chance to inspect the upload stub before
// it is deleted from external storage by aborting the multipart
// upload; see also ExternalUploadManager
if (file.meta.error && self.siteSettings.enable_upload_debug_mode) {
return;
}
return ajax("/uploads/abort-multipart.json", {
type: "POST",
data: {
external_upload_identifier: uploadId,
},
// uppy is inconsistent, an error here does not fire the upload-error event
}).catch((err) => {
self._handleUploadError(file, err);
});
},
// we will need a listParts function at some point when we want to
// resume multipart uploads; this is used by uppy to figure out
// what parts are uploaded and which still need to be
});
},
_reset() {
this._uppyInstance?.reset();
this.setProperties({

View File

@ -53,7 +53,7 @@ export default Mixin.create({
topicBody,
archetypeId: "private_message",
draftKey: Composer.NEW_PRIVATE_MESSAGE_KEY,
hasGroups: hasGroups,
hasGroups,
});
},
});

View File

@ -0,0 +1,159 @@
import Mixin from "@ember/object/mixin";
import { Promise } from "rsvp";
import { ajax } from "discourse/lib/ajax";
import AwsS3Multipart from "@uppy/aws-s3-multipart";
export default Mixin.create({
_useS3MultipartUploads() {
this.set("usingS3MultipartUploads", true);
const self = this;
const retryDelays = [0, 1000, 3000, 5000];
this._uppyInstance.use(AwsS3Multipart, {
// controls how many simultaneous _chunks_ are uploaded, not files,
// which in turn controls the minimum number of chunks presigned
// in each batch (limit / 2)
//
// the default, and minimum, chunk size is 5mb. we can control the
// chunk size via getChunkSize(file), so we may want to increase
// the chunk size for larger files
limit: 10,
retryDelays,
createMultipartUpload(file) {
self._uppyInstance.emit("create-multipart", file.id);
const data = {
file_name: file.name,
file_size: file.size,
upload_type: file.meta.upload_type,
metadata: file.meta,
};
// the sha1 checksum is set by the UppyChecksum plugin, except
// for in cases where the browser does not support the required
// crypto mechanisms or an error occurs. it is an additional layer
// of security, and not required.
if (file.meta.sha1_checksum) {
data.metadata = { "sha1-checksum": file.meta.sha1_checksum };
}
return ajax("/uploads/create-multipart.json", {
type: "POST",
data,
// uppy is inconsistent, an error here fires the upload-error event
}).then((responseData) => {
self._uppyInstance.emit("create-multipart-success", file.id);
file.meta.unique_identifier = responseData.unique_identifier;
return {
uploadId: responseData.external_upload_identifier,
key: responseData.key,
};
});
},
prepareUploadParts(file, partData) {
if (file.preparePartsRetryAttempts === undefined) {
file.preparePartsRetryAttempts = 0;
}
return ajax("/uploads/batch-presign-multipart-parts.json", {
type: "POST",
data: {
part_numbers: partData.partNumbers,
unique_identifier: file.meta.unique_identifier,
},
})
.then((data) => {
if (file.preparePartsRetryAttempts) {
delete file.preparePartsRetryAttempts;
self._consoleDebug(
`[uppy] Retrying batch fetch for ${file.id} was successful, continuing.`
);
}
return { presignedUrls: data.presigned_urls };
})
.catch((err) => {
const status = err.jqXHR.status;
// it is kind of ugly to have to track the retry attempts for
// the file based on the retry delays, but uppy's `retryable`
// function expects the rejected Promise data to be structured
// _just so_, and provides no interface for us to tell how many
// times the upload has been retried (which it tracks internally)
//
// if we exceed the attempts then there is no way that uppy will
// retry the upload once again, so in that case the alert can
// be safely shown to the user that their upload has failed.
if (file.preparePartsRetryAttempts < retryDelays.length) {
file.preparePartsRetryAttempts += 1;
const attemptsLeft =
retryDelays.length - file.preparePartsRetryAttempts + 1;
self._consoleDebug(
`[uppy] Fetching a batch of upload part URLs for ${file.id} failed with status ${status}, retrying ${attemptsLeft} more times...`
);
return Promise.reject({ source: { status } });
} else {
self._consoleDebug(
`[uppy] Fetching a batch of upload part URLs for ${file.id} failed too many times, throwing error.`
);
// uppy is inconsistent, an error here does not fire the upload-error event
self._handleUploadError(file, err);
}
});
},
completeMultipartUpload(file, data) {
self._uppyInstance.emit("complete-multipart", file.id);
const parts = data.parts.map((part) => {
return { part_number: part.PartNumber, etag: part.ETag };
});
return ajax("/uploads/complete-multipart.json", {
type: "POST",
contentType: "application/json",
data: JSON.stringify({
parts,
unique_identifier: file.meta.unique_identifier,
pasted: file.meta.pasted,
for_private_message: file.meta.for_private_message,
}),
// uppy is inconsistent, an error here fires the upload-error event
}).then((responseData) => {
self._uppyInstance.emit("complete-multipart-success", file.id);
return responseData;
});
},
abortMultipartUpload(file, { key, uploadId }) {
// if the user cancels the upload before the key and uploadId
// are stored from the createMultipartUpload response then they
// will not be set, and we don't have to abort the upload because
// it will not exist yet
if (!key || !uploadId) {
return;
}
// this gives us a chance to inspect the upload stub before
// it is deleted from external storage by aborting the multipart
// upload; see also ExternalUploadManager
if (file.meta.error && self.siteSettings.enable_upload_debug_mode) {
return;
}
return ajax("/uploads/abort-multipart.json", {
type: "POST",
data: {
external_upload_identifier: uploadId,
},
// uppy is inconsistent, an error here does not fire the upload-error event
}).catch((err) => {
self._handleUploadError(file, err);
});
},
// we will need a listParts function at some point when we want to
// resume multipart uploads; this is used by uppy to figure out
// what parts are uploaded and which still need to be
});
},
});

View File

@ -13,12 +13,13 @@ import DropTarget from "@uppy/drop-target";
import XHRUpload from "@uppy/xhr-upload";
import AwsS3 from "@uppy/aws-s3";
import UppyChecksum from "discourse/lib/uppy-checksum-plugin";
import UppyS3Multipart from "discourse/mixins/uppy-s3-multipart";
import { on } from "discourse-common/utils/decorators";
import { warn } from "@ember/debug";
export const HUGE_FILE_THRESHOLD_BYTES = 104_857_600; // 100MB
export default Mixin.create({
export default Mixin.create(UppyS3Multipart, {
uploading: false,
uploadProgress: 0,
_uppyInstance: null,
@ -26,13 +27,6 @@ export default Mixin.create({
_inProgressUploads: 0,
id: null,
// TODO (martin): currently used for backups to turn on auto upload and PUT/XML requests
// and for emojis to do sequential uploads, when we get to replacing those
// with uppy make sure this is used when initializing uppy
uploadOptions() {
return {};
},
uploadDone() {
warn("You should implement `uploadDone`", {
id: "discourse.upload.missing-upload-done",
@ -170,7 +164,7 @@ export default Mixin.create({
});
this._uppyInstance.on("upload-error", (file, error, response) => {
displayErrorForUpload(response, this.siteSettings, file.name);
displayErrorForUpload(response || error, this.siteSettings, file.name);
this._reset();
});
@ -184,7 +178,11 @@ export default Mixin.create({
this.siteSettings.enable_direct_s3_uploads &&
!this.preventDirectS3Uploads
) {
this._useS3Uploads();
if (this.useMultipartUploadsIfAvailable) {
this._useS3MultipartUploads();
} else {
this._useS3Uploads();
}
} else {
this._useXHRUploads();
}

View File

@ -157,7 +157,7 @@ const Bookmark = RestModel.extend({
return User.create({
username: post_user_username,
avatar_template: avatarTemplate,
name: name,
name,
});
},
});

View File

@ -28,7 +28,7 @@ const NavItem = EmberObject.extend({
count = 0;
}
let extra = { count: count };
let extra = { count };
const titleKey = count === 0 ? ".title" : ".title_with_count";
return emojiUnescape(

View File

@ -549,7 +549,7 @@ export default RestModel.extend({
post.setProperties({
post_number: topic.get("highest_post_number"),
topic: topic,
topic,
created_at: new Date(),
id: -1,
});
@ -847,6 +847,18 @@ export default RestModel.extend({
return resolved;
},
triggerLikedPost(postId, likesCount) {
const resolved = Promise.resolve();
const post = this.findLoadedPost(postId);
if (post) {
post.updateLikeCount(likesCount);
this.storePost(post);
}
return resolved;
},
triggerReadPost(postId, readersCount) {
const resolved = Promise.resolve();
resolved.then(() => {
@ -1063,7 +1075,7 @@ export default RestModel.extend({
const url = `/t/${this.get("topic.id")}/posts.json`;
let data = {
post_number: postNumber,
asc: asc,
asc,
include_suggested: includeSuggested,
};

View File

@ -219,7 +219,7 @@ const Post = RestModel.extend({
: "post.deleted_by_author_simple";
promise = cookAsync(I18n.t(key)).then((cooked) => {
this.setProperties({
cooked: cooked,
cooked,
can_delete: false,
can_permanently_delete: false,
version: this.version + 1,
@ -355,6 +355,37 @@ const Post = RestModel.extend({
}
},
updateLikeCount(count) {
let current_actions_summary = this.get("actions_summary");
let likeActionID = Site.current().post_action_types.find(
(a) => a.name_key === "like"
).id;
if (!this.actions_summary.find((entry) => entry.id === likeActionID)) {
let json = Post.munge({
id: this.id,
actions_summary: [
{
id: likeActionID,
count,
},
],
});
this.set(
"actions_summary",
Object.assign(current_actions_summary, json.actions_summary)
);
this.set("actionByName", json.actionByName);
this.set("likeAction", json.likeAction);
} else {
this.actions_summary.find(
(entry) => entry.id === likeActionID
).count = count;
this.actionByName["like"] = count;
this.likeAction.count = count;
}
},
revertToRevision(version) {
return ajax(`/posts/${this.id}/revisions/${version}/revert`, {
type: "PUT",

View File

@ -49,7 +49,7 @@ const TopicDetails = RestModel.extend({
return ajax("/t/" + this.get("topic.id") + "/remove-allowed-group", {
type: "PUT",
data: { name: name },
data: { name },
}).then(() => {
groups.removeObject(groups.findBy("name", name));
});
@ -61,7 +61,7 @@ const TopicDetails = RestModel.extend({
return ajax("/t/" + this.get("topic.id") + "/remove-allowed-user", {
type: "PUT",
data: { username: username },
data: { username },
}).then(() => {
users.removeObject(users.findBy("username", username));
});

View File

@ -155,9 +155,9 @@ UserBadge.reopenClass({
return ajax("/user_badges", {
type: "POST",
data: {
username: username,
username,
badge_id: badgeId,
reason: reason,
reason,
},
}).then(function (json) {
return UserBadge.createFromJson(json);

View File

@ -382,7 +382,7 @@ const User = RestModel.extend({
// TODO: We can remove this when migrated fully to rest model.
this.set("isSaving", true);
return ajax(userPath(`${this.username_lower}.json`), {
data: data,
data,
type: "PUT",
})
.then((result) => {
@ -1037,7 +1037,7 @@ User.reopenClass(Singleton, {
// Find a `User` for a given username.
findByUsername(username, options) {
const user = User.create({ username: username });
const user = User.create({ username });
return user.findDetails(options);
},

View File

@ -43,11 +43,6 @@ export default {
(/iPhone|iPod/.test(navigator.userAgent) || caps.isIpadOS) &&
!window.MSStream;
caps.isIOS15Safari =
caps.isIOS &&
/Version\/15/.test(navigator.userAgent) &&
!/DiscourseHub/.test(navigator.userAgent);
caps.hasContactPicker =
"contacts" in navigator && "ContactsManager" in window;

View File

@ -151,9 +151,9 @@ export default (filterArg, params) => {
canCreateTopic && category.get("permission") === PermissionType.FULL;
this.controllerFor("navigation/category").setProperties({
canCreateTopicOnCategory: canCreateTopicOnCategory,
canCreateTopicOnCategory,
cannotCreateTopicOnCategory: !canCreateTopicOnCategory,
canCreateTopic: canCreateTopic,
canCreateTopic,
});
let topicOpts = {
@ -165,8 +165,8 @@ export default (filterArg, params) => {
selected: [],
noSubcategories: params && !!params.no_subcategories,
expandAllPinned: true,
canCreateTopic: canCreateTopic,
canCreateTopicOnCategory: canCreateTopicOnCategory,
canCreateTopic,
canCreateTopicOnCategory,
};
const p = category.get("params");

View File

@ -61,7 +61,7 @@ export default (inboxType, filter) => {
.get("groups")
.filterBy("name", groupName)[0];
this.setProperties({ groupName: groupName, group });
this.setProperties({ groupName, group });
},
setupController() {

View File

@ -67,7 +67,7 @@ export default (inboxType, path, filter) => {
tagsForUser: this.modelFor("user").get("username_lower"),
selected: [],
showToggleBulkSelect: true,
filter: filter,
filter,
group: null,
inbox: inboxType,
});

View File

@ -30,7 +30,7 @@ export default DiscourseRoute.extend(OpenComposer, {
const period = User.currentProp("redirected_to_top.period") || "all";
this.replaceWith("discovery.top", {
queryParams: {
period: period,
period,
},
});
} else if (url && (matches = url.match(/top\/(.*)$/))) {

View File

@ -14,7 +14,7 @@ export default RestrictedUserRoute.extend({
setupController: function (controller, model) {
controller.reset();
controller.setProperties({
model: model,
model,
oldEmail: controller.new ? "" : model.get("email"),
newEmail: controller.new ? "" : model.get("email"),
});

View File

@ -221,7 +221,7 @@ export default DiscourseRoute.extend(FilterModeMixin, {
if (categoryId) {
options = $.extend({}, options, {
categoryId: categoryId,
categoryId,
includeSubcategories: !controller.noSubcategories,
});
}

View File

@ -183,10 +183,32 @@ const TopicRoute = DiscourseRoute.extend({
}
const topic = this.modelFor("topic");
if (topic && currentPost) {
let postUrl = topic.get("url");
let postUrl;
if (currentPost > 1) {
postUrl += "/" + currentPost;
postUrl = topic.urlForPostNumber(currentPost);
} else {
postUrl = topic.url;
}
if (this._router.currentRoute.queryParams) {
let searchParams;
Object.entries(this._router.currentRoute.queryParams).map(
([key, value]) => {
if (!searchParams) {
searchParams = new URLSearchParams();
}
searchParams.append(key, value);
}
);
if (searchParams) {
postUrl += `?${searchParams.toString()}`;
}
}
cancel(this.scheduledReplace);

View File

@ -24,7 +24,7 @@ export default DiscourseRoute.extend(ViewingActionType, {
this.get("currentUser.admin")
) {
return this.store.find("notification", {
username: username,
username,
filter: params.filter,
});
}

View File

@ -355,7 +355,7 @@ export default class PresenceService extends Service {
this._queuedEvents.push({
channel: channelName,
type: "enter",
promiseProxy: promiseProxy,
promiseProxy,
});
this._scheduleNextUpdate();
@ -384,7 +384,7 @@ export default class PresenceService extends Service {
this._queuedEvents.push({
channel: channelName,
type: "leave",
promiseProxy: promiseProxy,
promiseProxy,
});
this._scheduleNextUpdate();

View File

@ -1,4 +1,4 @@
<label class="btn" disabled={{uploading}} title={{i18n "admin.backups.upload.title"}}>
{{d-icon "upload"}}&nbsp;{{uploadButtonText}}
{{d-icon "upload"}}{{uploadButtonText}}
<input class="hidden-upload-field" disabled={{uploading}} type="file" accept=".gz">
</label>

View File

@ -0,0 +1,4 @@
<label class="btn" disabled={{uploading}} title={{i18n "admin.backups.upload.title"}}>
{{d-icon "upload"}}{{uploadButtonText}}
<input class="hidden-upload-field" disabled={{uploading}} type="file" accept=".gz">
</label>

View File

@ -48,6 +48,7 @@
uploadedAvatarId=user.custom_avatar_upload_id
uploading=uploading
class="avatar-uploader"
id="avatar-uploader"
done=(action "uploadComplete")}}
</div>
{{/if}}

View File

@ -1,3 +1,3 @@
{{#d-modal-body title="tagging.upload"}}
{{tags-uploader refresh=(route-action "triggerRefresh") closeModal=(route-action "closeModal")}}
{{tags-uploader refresh=(route-action "triggerRefresh") closeModal=(route-action "closeModal") id="tags-uploader"}}
{{/d-modal-body}}

View File

@ -103,7 +103,7 @@ export default createWidget("home-logo", {
: { src: getURL(url), alt: title };
const imgElement = h(`img#site-logo.${key}`, {
key: key,
key,
attributes,
});

View File

@ -38,9 +38,9 @@ function preloadInvite({
title: "team",
},
username: "invited",
email_verified_by_link: email_verified_by_link,
different_external_email: different_external_email,
hidden_email: hidden_email,
email_verified_by_link,
different_external_email,
hidden_email,
};
if (link) {

View File

@ -24,7 +24,7 @@ async function triggerSwipeStart(touchTarget) {
);
const touchStart = {
touchTarget: touchTarget,
touchTarget,
x:
zoom *
(touchTarget.getBoundingClientRect().x +

View File

@ -123,7 +123,7 @@ acceptance(
return helper.response({
topic_list: {
topics: topics,
topics,
},
});
});

View File

@ -8,7 +8,7 @@ import hbs from "htmlbars-inline-precompile";
const createArgs = (topic) => {
return {
topic: topic,
topic,
openUpwards: "true",
toggleMultiSelect: () => {},
deleteTopic: () => {},

View File

@ -149,7 +149,7 @@ function writeSummaryLine(message) {
if (window.Testem) {
window.Testem.useCustomAdapter(function (socket) {
socket.emit("test-metadata", "summary-line", {
message: message,
message,
});
});
}

View File

@ -107,8 +107,8 @@ window.keyEvent = function (selector, contextOrType, typeOrKeyCode, keyCode) {
let key = keyFromKeyCode[keyCode];
return window.triggerEvent(selector, context, type, {
keyCode: keyCode,
keyCode,
which: keyCode,
key: key,
key,
});
};

View File

@ -1011,7 +1011,7 @@ eviltrout</p>
"[test.pdf|attachment](upload://o8iobpLcW3WSFvVH7YQmyGlKmGM.pdf)",
{
siteSettings: { secure_media: false },
lookupUploadUrls: lookupUploadUrls,
lookupUploadUrls,
},
`<p><a class="attachment" href="/uploads/short-url/blah">test.pdf</a></p>`,
"It returns the correct attachment link HTML when the URL is mapped without secure media"
@ -1033,7 +1033,7 @@ eviltrout</p>
"[test.pdf|attachment](upload://o8iobpLcW3WSFvVH7YQmyGlKmGM.pdf)",
{
siteSettings: { secure_media: true },
lookupUploadUrls: lookupUploadUrls,
lookupUploadUrls,
},
`<p><a class="attachment" href="/secure-media-uploads/original/3X/c/b/o8iobpLcW3WSFvVH7YQmyGlKmGM.pdf">test.pdf</a></p>`,
"It returns the correct attachment link HTML when the URL is mapped with secure media"
@ -1067,7 +1067,7 @@ eviltrout</p>
"![baby shark|video](upload://eyPnj7UzkU0AkGkx2dx8G4YM1Jx.mp4)",
{
siteSettings: { secure_media: true },
lookupUploadUrls: lookupUploadUrls,
lookupUploadUrls,
},
`<p><div class="video-container">
<video width="100%" height="100%" preload="metadata" controls>
@ -1104,7 +1104,7 @@ eviltrout</p>
"![baby shark|audio](upload://eyPnj7UzkU0AkGkx2dx8G4YM1Jx.mp3)",
{
siteSettings: { secure_media: true },
lookupUploadUrls: lookupUploadUrls,
lookupUploadUrls,
},
`<p><audio preload="metadata" controls>
<source src="/secure-media-uploads/original/3X/c/b/test.mp3">

View File

@ -103,14 +103,14 @@ discourseModule("Unit | Utilities", function () {
let avatarTemplate = "/path/to/avatar/{size}.png";
assert.strictEqual(
avatarImg({ avatarTemplate: avatarTemplate, size: "tiny" }),
avatarImg({ avatarTemplate, size: "tiny" }),
"<img loading='lazy' alt='' width='20' height='20' src='/path/to/avatar/40.png' class='avatar'>",
"it returns the avatar html"
);
assert.strictEqual(
avatarImg({
avatarTemplate: avatarTemplate,
avatarTemplate,
size: "tiny",
title: "evilest trout",
}),
@ -120,7 +120,7 @@ discourseModule("Unit | Utilities", function () {
assert.strictEqual(
avatarImg({
avatarTemplate: avatarTemplate,
avatarTemplate,
size: "tiny",
extraClasses: "evil fish",
}),

View File

@ -39,7 +39,7 @@ module("Unit | Model | category", function () {
slugFor(
store.createRecord("category", {
slug: "luke",
parentCategory: parentCategory,
parentCategory,
}),
"darth/luke",
"it uses the parent slug before the child"
@ -48,7 +48,7 @@ module("Unit | Model | category", function () {
slugFor(
store.createRecord("category", {
id: 555,
parentCategory: parentCategory,
parentCategory,
}),
"darth/555-category",
"it uses the parent slug before the child and then uses id"
@ -58,7 +58,7 @@ module("Unit | Model | category", function () {
slugFor(
store.createRecord("category", {
id: 555,
parentCategory: parentCategory,
parentCategory,
}),
"345-category/555-category",
"it uses the parent before the child and uses ids for both"

View File

@ -277,7 +277,7 @@ discourseModule("Unit | Model | composer", function () {
assert.ok(!composer.get("editingFirstPost"), "it's false by default");
const post = Post.create({ id: 123, post_number: 2 });
composer.setProperties({ post: post, action: EDIT });
composer.setProperties({ post, action: EDIT });
assert.ok(
!composer.get("editingFirstPost"),
"it's false when not editing the first post"
@ -337,7 +337,7 @@ discourseModule("Unit | Model | composer", function () {
action: REPLY,
draftKey: "asfd",
draftSequence: 1,
quote: quote,
quote,
});
};
@ -363,7 +363,7 @@ discourseModule("Unit | Model | composer", function () {
post_number: 2,
static_doc: true,
});
composer.setProperties({ post: post, action: EDIT });
composer.setProperties({ post, action: EDIT });
composer.set("title", "asdf");
assert.ok(composer.get("titleLengthValid"), "admins can use short titles");

View File

@ -889,7 +889,7 @@ module("Unit | Model | post-stream", function () {
[1, 2, 3, 5].forEach((id) => {
postStream.appendPost(
store.createRecord("post", { id: id, post_number: id })
store.createRecord("post", { id, post_number: id })
);
});

View File

@ -59,7 +59,7 @@ module("Unit | Model | post", function () {
test("destroy by staff", async function (assert) {
let user = User.create({ username: "staff", moderator: true });
let post = buildPost({ user: user });
let post = buildPost({ user });
await post.destroy(user);
@ -85,7 +85,7 @@ module("Unit | Model | post", function () {
test("destroy by non-staff", async function (assert) {
const originalCooked = "this is the original cooked value";
const user = User.create({ username: "evil trout" });
const post = buildPost({ user: user, cooked: originalCooked });
const post = buildPost({ user, cooked: originalCooked });
await post.destroy(user);

View File

@ -22,7 +22,7 @@ module("Unit | Model | user", function () {
assert.deepEqual(
user.get("searchContext"),
{ type: "user", id: "eviltrout", user: user },
{ type: "user", id: "eviltrout", user },
"has a search context"
);
});

View File

@ -179,9 +179,6 @@ table.api-keys {
width: 50%;
}
.scopes-title {
font-size: $font-up-2;
font-weight: bold;
text-decoration: underline;
margin-top: 20px;
}
}

View File

@ -584,7 +584,9 @@ button {
/* start state */
.mfp-content {
opacity: 0;
transition: all 0.2s;
@media screen and (prefers-reduced-motion: no-preference) {
transition: all 0.2s;
}
-webkit-transform: scale(0.8);
-ms-transform: scale(0.8);
transform: scale(0.8);

View File

@ -12,10 +12,11 @@
}
.modal-inner-container {
--modal-max-width: 47em; // set in ems to scale with user font-size
box-sizing: border-box;
flex: 0 1 auto;
margin: 0 auto;
max-width: 700px;
max-width: var(--modal-max-width);
background-color: var(--secondary);
box-shadow: shadow("modal");
@ -273,7 +274,7 @@
pre code {
white-space: pre-wrap;
max-width: 700px;
max-width: var(--modal-max-width);
}
}
@ -364,7 +365,7 @@
}
.incoming-email-content {
height: 300px;
max-width: 700px;
max-width: 100%;
width: 90vw; // forcing textarea wider
textarea,
.incoming-email-html-part {
@ -372,6 +373,7 @@
border: none;
border-top: 1px solid var(--primary-low);
padding-top: 10px;
width: 100%;
}
textarea {
font-family: monospace;
@ -427,7 +429,8 @@
}
.change-timestamp {
max-width: 420px;
width: 28em; // scales with user font-size
max-width: 90vw; // prevents overflow due to extra large user font-size
}
.change-timestamp {

View File

@ -1,18 +1,15 @@
// styles that apply to the "share" modal & popup when sharing a link to a post or topic
.link-share-container {
.link-share-container,
.notify-user-input {
display: flex;
button {
.btn {
margin-left: 0.5em;
transition-property: background-color, color; // don't transition outline
}
input {
input,
.select-kit {
width: 100%;
font-size: var(--font-up-1);
margin-bottom: 0;
&:focus + button {
outline: 1px solid var(--tertiary);
}
}
}
@ -62,7 +59,8 @@
box-shadow: shadow("card");
background-color: var(--secondary);
padding: 0.5em;
width: 300px;
width: 20em; // scales with user font-size
max-width: 100vw; // prevents overflow due to extra large user font-size
display: none;
&.visible {
display: block;
@ -114,23 +112,6 @@
}
}
.notify-user-input {
display: flex;
align-items: stretch;
.select-kit {
width: 100%;
}
.multi-select-header {
width: 100%;
}
.select-kit.multi-select .choices {
padding: 0;
}
}
// topic invite modal
.create-invite-modal {

View File

@ -112,6 +112,7 @@
.create-invite-bulk-modal,
.share-topic-modal {
.modal-inner-container {
width: 600px;
width: 40em; // scale with user font-size
max-width: 100vw; // prevent overflow if user font-size is enourmous
}
}

View File

@ -32,13 +32,6 @@
padding-bottom: 0px;
}
// iOS 15 Safari has a floating address bar that displays above the composer submit bar
// we cannot detect its preseence, so we need to add extra padding
// Apple says it's a known issue, see https://bugs.webkit.org/show_bug.cgi?id=229876
.keyboard-visible body.ios-safari-15-hack &.open .reply-area {
padding-bottom: 45px;
}
.reply-to {
margin: 5px 0;
.reply-details {

View File

@ -255,10 +255,12 @@ class UploadsController < ApplicationController
external_upload_manager = ExternalUploadManager.new(external_upload_stub, opts)
hijack do
begin
upload = external_upload_manager.promote_to_upload!
upload = external_upload_manager.transform!
if upload.errors.empty?
response_serialized = external_upload_stub.upload_type != "backup" ? UploadsController.serialize_upload(upload) : {}
external_upload_stub.destroy!
render json: UploadsController.serialize_upload(upload), status: 200
render json: response_serialized, status: 200
else
render_json_error(upload.errors.to_hash.values.flatten, status: 422)
end
@ -301,11 +303,17 @@ class UploadsController < ApplicationController
file_size = params.require(:file_size).to_i
upload_type = params.require(:upload_type)
if file_size_too_big?(file_name, file_size)
return render_json_error(
I18n.t("upload.attachments.too_large_humanized", max_size: ActiveSupport::NumberHelper.number_to_human_size(SiteSetting.max_attachment_size_kb.kilobytes)),
status: 422
)
if upload_type == "backup"
ensure_staff
return render_json_error(I18n.t("backup.backup_file_should_be_tar_gz")) unless valid_backup_extension?(file_name)
return render_json_error(I18n.t("backup.invalid_filename")) unless valid_backup_filename?(file_name)
else
if file_size_too_big?(file_name, file_size)
return render_json_error(
I18n.t("upload.attachments.too_large_humanized", max_size: ActiveSupport::NumberHelper.number_to_human_size(SiteSetting.max_attachment_size_kb.kilobytes)),
status: 422
)
end
end
begin
@ -321,6 +329,13 @@ class UploadsController < ApplicationController
debug_upload_error(err, "upload.create_mutlipart_failure", additional_detail: err.message),
status: 422
)
rescue BackupRestore::BackupStore::BackupFileExists
return render_json_error(I18n.t("backup.file_exists"), status: 422)
rescue BackupRestore::BackupStore::StorageError => err
return render_json_error(
debug_upload_error(err, "upload.create_mutlipart_failure", additional_detail: err.message),
status: 422
)
end
render json: external_upload_data
@ -347,9 +362,11 @@ class UploadsController < ApplicationController
return render_404
end
store = multipart_store(external_upload_stub.upload_type)
presigned_urls = {}
part_numbers.each do |part_number|
presigned_urls[part_number] = Discourse.store.presign_multipart_part(
presigned_urls[part_number] = store.presign_multipart_part(
upload_id: external_upload_stub.external_upload_identifier,
key: external_upload_stub.key,
part_number: part_number
@ -370,9 +387,12 @@ class UploadsController < ApplicationController
end
def multipart_upload_exists?(external_upload_stub)
store = multipart_store(external_upload_stub.upload_type)
begin
Discourse.store.list_multipart_parts(
upload_id: external_upload_stub.external_upload_identifier, key: external_upload_stub.key
store.list_multipart_parts(
upload_id: external_upload_stub.external_upload_identifier,
key: external_upload_stub.key,
max_parts: 1
)
rescue Aws::S3::Errors::NoSuchUpload => err
debug_upload_error(err, "upload.external_upload_not_found", { additional_detail: "path: #{external_upload_stub.key}" })
@ -393,9 +413,10 @@ class UploadsController < ApplicationController
return render json: success_json if external_upload_stub.blank?
return render_404 if external_upload_stub.created_by_id != current_user.id
store = multipart_store(external_upload_stub.upload_type)
begin
Discourse.store.abort_multipart(
store.abort_multipart(
upload_id: external_upload_stub.external_upload_identifier,
key: external_upload_stub.key
)
@ -428,6 +449,7 @@ class UploadsController < ApplicationController
return render_404
end
store = multipart_store(external_upload_stub.upload_type)
parts = parts.map do |part|
part_number = part[:part_number]
etag = part[:etag]
@ -445,7 +467,7 @@ class UploadsController < ApplicationController
end
begin
complete_response = Discourse.store.complete_multipart(
complete_response = store.complete_multipart(
upload_id: external_upload_stub.external_upload_identifier,
key: external_upload_stub.key,
parts: parts
@ -462,6 +484,11 @@ class UploadsController < ApplicationController
protected
def multipart_store(upload_type)
ensure_staff if upload_type == "backup"
ExternalUploadManager.store_for_upload_type(upload_type)
end
def force_download?
params[:dl] == "1"
end
@ -583,4 +610,12 @@ class UploadsController < ApplicationController
return if metadata.blank?
metadata.permit("sha1-checksum").to_h
end
def valid_backup_extension?(filename)
/\.(tar\.gz|t?gz)$/i =~ filename
end
def valid_backup_filename?(filename)
!!(/^[a-zA-Z0-9\._\-]+$/ =~ filename)
end
end

View File

@ -17,6 +17,9 @@ class ApiKeyScope < ActiveRecord::Base
return @default_mappings unless @default_mappings.nil?
mappings = {
global: {
read: { methods: %i[get] }
},
topics: {
write: { actions: %w[posts#create], params: %i[topic_id] },
read: {
@ -48,12 +51,7 @@ class ApiKeyScope < ActiveRecord::Base
}
}
mappings.each_value do |resource_actions|
resource_actions.each_value do |action_data|
action_data[:urls] = find_urls(action_data[:actions])
end
end
parse_resources!(mappings)
@default_mappings = mappings
end
@ -62,33 +60,48 @@ class ApiKeyScope < ActiveRecord::Base
return default_mappings if plugin_mappings.empty?
default_mappings.deep_dup.tap do |mappings|
plugin_mappings.each do |resource|
resource.each_value do |resource_actions|
resource_actions.each_value do |action_data|
action_data[:urls] = find_urls(action_data[:actions])
end
end
mappings.deep_merge!(resource)
plugin_mappings.each do |plugin_mapping|
parse_resources!(plugin_mapping)
mappings.deep_merge!(plugin_mapping)
end
end
end
def find_urls(actions)
Rails.application.routes.routes.reduce([]) do |memo, 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]
def parse_resources!(mappings)
mappings.each_value do |resource_actions|
resource_actions.each_value do |action_data|
action_data[:urls] = find_urls(actions: action_data[:actions], methods: action_data[:methods])
end
end
end
memo.tap do |m|
if actions.include?(action) && api_supported_path && !excluded_paths.include?(path)
m << "#{path} (#{route.verb})"
def find_urls(actions:, methods:)
action_urls = []
method_urls = []
if actions.present?
Rails.application.routes.routes.reduce([]) do |memo, 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
end
end
end
if methods.present?
methods.each do |method|
method_urls << "* (#{method})"
end
end
action_urls + method_urls
end
end

Some files were not shown because too many files have changed in this diff Show More