discourse-shared-edits/assets/javascripts/discourse/initializers/shared-edits-init.js
Sam 4e51a4e231
Some checks failed
Discourse Plugin / ci (push) Has been cancelled
FEATURE: allow shared edits to be enabled on any group (#163)
Allow sites to choose which groups can enable or disable shared edits instead of hardcoding staff and TL4 users. Expose the permission on the current user serializer so the admin menu can hide toggle actions for unauthorized users.

Add rate limits to privileged reset and recover endpoints, and update specs, acceptance tests, docs, and settings copy for the new permission model.

* Update config/settings.yml

Co-authored-by: Martin Brennan <martin@discourse.org>

* fix spec

---------

Co-authored-by: Martin Brennan <martin@discourse.org>
2026-06-25 16:28:07 +10:00

230 lines
5.9 KiB
JavaScript

import { action } from "@ember/object";
import { service } from "@ember/service";
import { trustHTML } from "@ember/template";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { USER_OPTION_COMPOSITION_MODES } from "discourse/lib/constants";
import { iconHTML } from "discourse/lib/icon-library";
import { withPluginApi } from "discourse/lib/plugin-api";
import {
registerCustomizationCallback,
SAVE_ICONS,
SAVE_LABELS,
} from "discourse/models/composer";
import SharedEditButton from "../components/shared-edit-button";
import sharedEditsProsemirrorExtension from "../lib/shared-edits-prosemirror-extension";
const SHARED_EDIT_ACTION = "sharedEdit";
function formatSharedEditActionTitle(model) {
if (model.action !== SHARED_EDIT_ACTION) {
return;
}
const opts = model.replyOptions;
if (!opts?.userAvatar || !opts?.userLink || !opts?.postLink) {
return;
}
return trustHTML(`
${iconHTML("far-pen-to-square", { title: "shared_edits.composer_title" })}
<a class="post-link" href="${opts.postLink.href}">${opts.postLink.anchor}</a>
${opts.userAvatar}
<span class="username">${opts.userLink.anchor}</span>
`);
}
function initWithApi(api, siteSettings) {
SAVE_LABELS[SHARED_EDIT_ACTION] = "composer.save_edit";
SAVE_ICONS[SHARED_EDIT_ACTION] = "pencil";
registerCustomizationCallback({
actionTitle: formatSharedEditActionTitle,
});
api.registerValueTransformer(
"composer-force-editor-mode",
({ value, context }) => {
if (context.model?.action === SHARED_EDIT_ACTION) {
if (siteSettings?.shared_edits_editor_mode === "rich") {
return USER_OPTION_COMPOSITION_MODES.rich;
}
return USER_OPTION_COMPOSITION_MODES.markdown;
}
return value;
}
);
api.registerRichEditorExtension(sharedEditsProsemirrorExtension);
customizePostMenu(api);
const currentUser = api.getCurrentUser();
api.addPostAdminMenuButton((attrs) => {
if (!currentUser?.can_toggle_shared_edits) {
return;
}
return {
icon: "far-pen-to-square",
className: "admin-toggle-shared-edits",
label: attrs.shared_edits_enabled
? "shared_edits.disable_shared_edits"
: "shared_edits.enable_shared_edits",
action: async (post) => {
const url = `/shared_edits/p/${post.id}/${
post.shared_edits_enabled ? "disable" : "enable"
}.json`;
try {
await ajax(url, { type: "PUT" });
post.set("shared_edits_enabled", !post.shared_edits_enabled);
} catch (e) {
popupAjaxError(e);
}
},
};
});
api.addTrackedPostProperties("shared_edits_enabled");
api.addPostClassesCallback((attrs) => {
if (attrs.shared_edits_enabled && attrs.canEdit) {
return ["shared-edits-post"];
}
});
api.modifyClass(
"model:composer",
(Superclass) =>
class extends Superclass {
get creatingSharedEdit() {
return this.get("action") === SHARED_EDIT_ACTION;
}
get editingPost() {
return super.editingPost || this.creatingSharedEdit;
}
}
);
api.modifyClass(
"controller:topic",
(Superclass) =>
class extends Superclass {
init() {
super.init(...arguments);
this.appEvents.on(
"shared-edit-on-post",
this,
this._handleSharedEditOnPost
);
}
willDestroy() {
super.willDestroy(...arguments);
this.appEvents.off(
"shared-edit-on-post",
this,
this._handleSharedEditOnPost
);
}
async _handleSharedEditOnPost(post) {
const draftKey = post.get("topic.draft_key");
const draftSequence = post.get("topic.draft_sequence");
let raw;
try {
const result = await ajax(`/posts/${post.id}.json`);
raw = result.raw;
} catch (e) {
popupAjaxError(e);
return;
}
this.get("composer").open({
post,
action: SHARED_EDIT_ACTION,
draftKey,
draftSequence,
reply: raw,
});
}
}
);
api.modifyClass(
"component:d-editor",
(Superclass) =>
class extends Superclass {
@service composer;
@service sharedEditManager;
@action
onChange(event) {
super.onChange(event);
if (this.composer?.model?.action !== SHARED_EDIT_ACTION) {
return;
}
this.sharedEditManager?.syncFromComposerValue?.(
event?.target?.value ?? ""
);
}
}
);
api.modifyClass(
"component:composer-messages",
(Superclass) =>
class extends Superclass {
async _findMessages() {
if (this.composer?.action === SHARED_EDIT_ACTION) {
this.set("checkedMessages", true);
return;
}
return super._findMessages(...arguments);
}
}
);
}
function customizePostMenu(api) {
api.registerValueTransformer(
"post-menu-buttons",
({ value: dag, context: { post, buttonLabels, buttonKeys } }) => {
if (!post.shared_edits_enabled || !post.canEdit) {
return;
}
dag.replace(buttonKeys.EDIT, SharedEditButton, {
after: [buttonKeys.SHOW_MORE, buttonKeys.REPLY],
});
dag.reposition(buttonKeys.REPLY, {
after: buttonKeys.SHOW_MORE,
before: buttonKeys.EDIT,
});
buttonLabels.hide(buttonKeys.REPLY);
}
);
api.addTrackedPostProperties("shared_edits_enabled");
}
export default {
name: "discourse-shared-edits",
initialize: (container) => {
const siteSettings = container.lookup("service:site-settings");
if (!siteSettings.shared_edits_enabled) {
return;
}
withPluginApi((api) => initWithApi(api, siteSettings));
},
};