Merge branch 'main' into generic-import
This commit is contained in:
commit
1c31741a0b
@ -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)
|
||||
|
||||
@ -48,7 +48,7 @@ export default Component.extend({
|
||||
}
|
||||
// update the formatted logs & cache index
|
||||
this.setProperties({
|
||||
formattedLogs: formattedLogs,
|
||||
formattedLogs,
|
||||
index: logs.length,
|
||||
});
|
||||
// force rerender
|
||||
|
||||
@ -24,7 +24,7 @@ export default Component.extend({
|
||||
|
||||
const config = {
|
||||
type: this.type,
|
||||
data: data,
|
||||
data,
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
|
||||
@ -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();
|
||||
},
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -121,7 +121,7 @@ export default Controller.extend({
|
||||
},
|
||||
|
||||
filterBySubject(subject) {
|
||||
this.changeFilters({ subject: subject });
|
||||
this.changeFilters({ subject });
|
||||
},
|
||||
|
||||
exportStaffActionLogs() {
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -592,7 +592,7 @@ export default Controller.extend(CanCheckEmails, {
|
||||
(deletedPosts * 100) / user.get("post_count")
|
||||
);
|
||||
progressModal.setProperties({
|
||||
deletedPercentage: deletedPercentage,
|
||||
deletedPercentage,
|
||||
});
|
||||
performDelete(progressModal);
|
||||
}
|
||||
|
||||
@ -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));
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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))
|
||||
);
|
||||
|
||||
@ -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.[]")
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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"),
|
||||
|
||||
@ -24,7 +24,7 @@ export default Route.extend({
|
||||
controller.setProperties({
|
||||
siteText,
|
||||
saved: false,
|
||||
localeFullName: localeFullName,
|
||||
localeFullName,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
{{emoji-uploader
|
||||
emojiGroups=emojiGroups
|
||||
done=(action "emojiUploaded")
|
||||
id="emoji-uploader"
|
||||
}}
|
||||
|
||||
<hr>
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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"),
|
||||
},
|
||||
|
||||
@ -83,7 +83,7 @@ export default Component.extend({
|
||||
!topic.archived &&
|
||||
!topic.closed &&
|
||||
!topic.deleted,
|
||||
topic: topic,
|
||||
topic,
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -77,7 +77,12 @@ export default MountWidget.extend({
|
||||
if (this.isDestroyed || this.isDestroying) {
|
||||
return;
|
||||
}
|
||||
if (isWorkaroundActive()) {
|
||||
|
||||
if (
|
||||
isWorkaroundActive() ||
|
||||
document.webkitFullscreenElement ||
|
||||
document.fullscreenElement
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -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(""),
|
||||
|
||||
@ -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();
|
||||
},
|
||||
});
|
||||
@ -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);
|
||||
}
|
||||
);
|
||||
|
||||
@ -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));
|
||||
|
||||
|
||||
@ -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);
|
||||
});
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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,
|
||||
});
|
||||
});
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -121,7 +121,7 @@ export default Controller.extend(PasswordValidation, {
|
||||
this.setProperties({
|
||||
securityKeyRequired: true,
|
||||
password: null,
|
||||
errorMessage: errorMessage,
|
||||
errorMessage,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -60,7 +60,7 @@ export default Controller.extend(BulkTopicSelection, {
|
||||
|
||||
const opts = {
|
||||
inbox: this.inbox,
|
||||
topicIds: topicIds,
|
||||
topicIds,
|
||||
};
|
||||
|
||||
if (this.group) {
|
||||
|
||||
@ -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,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@ -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 "";
|
||||
|
||||
@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -27,10 +27,10 @@ export function extractLinkMeta(topic) {
|
||||
}
|
||||
|
||||
const meta = {
|
||||
target: target,
|
||||
target,
|
||||
href,
|
||||
domain: domain,
|
||||
rel: rel,
|
||||
domain,
|
||||
rel,
|
||||
};
|
||||
|
||||
if (_decorators.length) {
|
||||
|
||||
@ -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()) {
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -53,7 +53,7 @@ export default Mixin.create({
|
||||
topicBody,
|
||||
archetypeId: "private_message",
|
||||
draftKey: Composer.NEW_PRIVATE_MESSAGE_KEY,
|
||||
hasGroups: hasGroups,
|
||||
hasGroups,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
159
app/assets/javascripts/discourse/app/mixins/uppy-s3-multipart.js
Normal file
159
app/assets/javascripts/discourse/app/mixins/uppy-s3-multipart.js
Normal 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
|
||||
});
|
||||
},
|
||||
});
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -157,7 +157,7 @@ const Bookmark = RestModel.extend({
|
||||
return User.create({
|
||||
username: post_user_username,
|
||||
avatar_template: avatarTemplate,
|
||||
name: name,
|
||||
name,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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));
|
||||
});
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
},
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -61,7 +61,7 @@ export default (inboxType, filter) => {
|
||||
.get("groups")
|
||||
.filterBy("name", groupName)[0];
|
||||
|
||||
this.setProperties({ groupName: groupName, group });
|
||||
this.setProperties({ groupName, group });
|
||||
},
|
||||
|
||||
setupController() {
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
@ -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\/(.*)$/))) {
|
||||
|
||||
@ -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"),
|
||||
});
|
||||
|
||||
@ -221,7 +221,7 @@ export default DiscourseRoute.extend(FilterModeMixin, {
|
||||
|
||||
if (categoryId) {
|
||||
options = $.extend({}, options, {
|
||||
categoryId: categoryId,
|
||||
categoryId,
|
||||
includeSubcategories: !controller.noSubcategories,
|
||||
});
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -24,7 +24,7 @@ export default DiscourseRoute.extend(ViewingActionType, {
|
||||
this.get("currentUser.admin")
|
||||
) {
|
||||
return this.store.find("notification", {
|
||||
username: username,
|
||||
username,
|
||||
filter: params.filter,
|
||||
});
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<label class="btn" disabled={{uploading}} title={{i18n "admin.backups.upload.title"}}>
|
||||
{{d-icon "upload"}} {{uploadButtonText}}
|
||||
{{d-icon "upload"}}{{uploadButtonText}}
|
||||
<input class="hidden-upload-field" disabled={{uploading}} type="file" accept=".gz">
|
||||
</label>
|
||||
|
||||
@ -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>
|
||||
@ -48,6 +48,7 @@
|
||||
uploadedAvatarId=user.custom_avatar_upload_id
|
||||
uploading=uploading
|
||||
class="avatar-uploader"
|
||||
id="avatar-uploader"
|
||||
done=(action "uploadComplete")}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -24,7 +24,7 @@ async function triggerSwipeStart(touchTarget) {
|
||||
);
|
||||
|
||||
const touchStart = {
|
||||
touchTarget: touchTarget,
|
||||
touchTarget,
|
||||
x:
|
||||
zoom *
|
||||
(touchTarget.getBoundingClientRect().x +
|
||||
|
||||
@ -123,7 +123,7 @@ acceptance(
|
||||
|
||||
return helper.response({
|
||||
topic_list: {
|
||||
topics: topics,
|
||||
topics,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@ -8,7 +8,7 @@ import hbs from "htmlbars-inline-precompile";
|
||||
|
||||
const createArgs = (topic) => {
|
||||
return {
|
||||
topic: topic,
|
||||
topic,
|
||||
openUpwards: "true",
|
||||
toggleMultiSelect: () => {},
|
||||
deleteTopic: () => {},
|
||||
|
||||
@ -149,7 +149,7 @@ function writeSummaryLine(message) {
|
||||
if (window.Testem) {
|
||||
window.Testem.useCustomAdapter(function (socket) {
|
||||
socket.emit("test-metadata", "summary-line", {
|
||||
message: message,
|
||||
message,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
@ -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>
|
||||
"",
|
||||
{
|
||||
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>
|
||||
"",
|
||||
{
|
||||
siteSettings: { secure_media: true },
|
||||
lookupUploadUrls: lookupUploadUrls,
|
||||
lookupUploadUrls,
|
||||
},
|
||||
`<p><audio preload="metadata" controls>
|
||||
<source src="/secure-media-uploads/original/3X/c/b/test.mp3">
|
||||
|
||||
@ -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",
|
||||
}),
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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 })
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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"
|
||||
);
|
||||
});
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
Reference in New Issue
Block a user