diff --git a/reviewboard/reviews/actions.py b/reviewboard/reviews/actions.py
index 3f67038def30e388ca1d7f5d7ff9f7ff7417c3a2..42d942da43f0c1e880e9386ac80aa60147597cee 100644
--- a/reviewboard/reviews/actions.py
+++ b/reviewboard/reviews/actions.py
@@ -85,7 +85,8 @@ class CloseCompletedAction(BaseAction):
     parent_id = CloseMenuAction.action_id
     label = _('Completed')
     apply_to = all_review_request_url_names
-    js_view_class = 'RB.CloseCompletedActionView'
+    js_model_class = 'RB.CloseCompletedAction'
+    js_view_class = 'RB.Actions.MenuItemActionView'
 
     def should_render(
         self,
@@ -125,7 +126,8 @@ class CloseDiscardedAction(BaseAction):
     parent_id = CloseMenuAction.action_id
     label = _('Discarded')
     apply_to = all_review_request_url_names
-    js_view_class = 'RB.CloseDiscardedActionView'
+    js_model_class = 'RB.CloseDiscardedAction'
+    js_view_class = 'RB.Actions.MenuItemActionView'
 
 
 class DeleteAction(BaseAction):
@@ -139,7 +141,8 @@ class DeleteAction(BaseAction):
     parent_id = CloseMenuAction.action_id
     label = _('Delete Permanently')
     apply_to = all_review_request_url_names
-    js_view_class = 'RB.DeleteActionView'
+    js_model_class = 'RB.DeleteAction'
+    js_view_class = 'RB.Actions.MenuItemActionView'
 
     def should_render(
         self,
@@ -332,7 +335,8 @@ class CreateReviewAction(BaseAction):
           'create a new review for you.'),
     ]
     icon_class = 'rb-icon rb-icon-create-review'
-    js_view_class = 'RB.CreateReviewActionView'
+    js_model_class = 'RB.CreateReviewAction'
+    js_view_class = 'RB.Actions.MenuItemActionView'
     template_name = 'actions/detailed_menuitem_action.html'
 
     def should_render(
@@ -378,7 +382,8 @@ class EditReviewAction(BaseAction):
         _('Edit your comments and publish your review.'),
     ]
     icon_class = 'rb-icon rb-icon-compose-review'
-    js_view_class = 'RB.EditReviewActionView'
+    js_model_class = 'RB.EditReviewAction'
+    js_view_class = 'RB.Actions.MenuItemActionView'
     template_name = 'actions/detailed_menuitem_action.html'
 
     def should_render(
@@ -425,7 +430,8 @@ class AddGeneralCommentAction(BaseAction):
           'any code or file attachments.'),
     ]
     icon_class = 'rb-icon rb-icon-edit'
-    js_view_class = 'RB.AddGeneralCommentActionView'
+    js_model_class = 'RB.AddGeneralCommentAction'
+    js_view_class = 'RB.Actions.MenuItemActionView'
     template_name = 'actions/detailed_menuitem_action.html'
 
     def should_render(
@@ -473,7 +479,8 @@ class ShipItAction(BaseAction):
           'a new review" above.'),
     ]
     icon_class = 'rb-icon rb-icon-shipit'
-    js_view_class = 'RB.ShipItActionView'
+    js_model_class = 'RB.ShipItAction'
+    js_view_class = 'RB.Actions.MenuItemActionView'
     template_name = 'actions/detailed_menuitem_action.html'
 
     def should_render(
@@ -756,7 +763,8 @@ class UploadDiffAction(BaseAction):
     action_id = 'upload-diff'
     parent_id = UpdateMenuAction.action_id
     apply_to = all_review_request_url_names
-    js_view_class = 'RB.UpdateDiffActionView'
+    js_model_class = 'RB.UpdateDiffAction'
+    js_view_class = 'RB.Actions.MenuItemActionView'
 
     def get_label(
         self,
@@ -825,7 +833,8 @@ class UploadFileAction(BaseAction):
     parent_id = UpdateMenuAction.action_id
     label = _('Add File')
     apply_to = all_review_request_url_names
-    js_view_class = 'RB.AddFileActionView'
+    js_model_class = 'RB.AddFileAction'
+    js_view_class = 'RB.Actions.MenuItemActionView'
 
     def should_render(
         self,
@@ -958,7 +967,8 @@ class ArchiveAction(BaseAction):
     parent_id = ArchiveMenuAction.action_id
     attachment = AttachmentPoint.REVIEW_REQUEST_LEFT
     apply_to = all_review_request_url_names
-    js_view_class = 'RB.ArchiveActionView'
+    js_model_class = 'RB.ArchiveAction'
+    js_view_class = 'RB.Actions.MenuItemActionView'
 
     # This is only shown on page load. It will be overridden at runtime.
     label = _('Toggle Archived')
@@ -975,7 +985,8 @@ class MuteAction(BaseAction):
     parent_id = ArchiveMenuAction.action_id
     attachment = AttachmentPoint.REVIEW_REQUEST_LEFT
     apply_to = all_review_request_url_names
-    js_view_class = 'RB.MuteActionView'
+    js_model_class = 'RB.MuteAction'
+    js_view_class = 'RB.Actions.MenuItemActionView'
 
     # This is only shown on page load. It will be overridden at runtime.
     label = _('Toggle Muted')
diff --git a/reviewboard/static/rb/js/common/actions/models/actionModel.ts b/reviewboard/static/rb/js/common/actions/models/actionModel.ts
index 7f5eec0d3e25e97b44b364882de0da49628c627e..08232d88d3b1ea98c77e6f59f6a0c82c02ee5a68 100644
--- a/reviewboard/static/rb/js/common/actions/models/actionModel.ts
+++ b/reviewboard/static/rb/js/common/actions/models/actionModel.ts
@@ -94,6 +94,11 @@ export interface ActionAttrs {
  * Subclasses may add their own attributes by passing in their own attribute
  * interface when extending this.
  *
+ * Version Changed:
+ *     7.1:
+ *     This is now the preferred place to put any action activation code.
+ *     Multiple views could wrap a single action.
+ *
  * Version Added:
  *     6.0
  */
@@ -112,4 +117,18 @@ export class Action<
         url: null,
         visible: false,
     };
+
+    /**
+     * Activate the action.
+     *
+     * This can be invoked by action views to enable default behaviors.
+     *
+     * By default, this does nothing.
+     *
+     * Version Added:
+     *     7.1
+     */
+    activate() {
+        // This can be overridden by subclasses for actions.
+    }
 }
diff --git a/reviewboard/static/rb/js/common/actions/views/actionView.ts b/reviewboard/static/rb/js/common/actions/views/actionView.ts
index a567dcbf732bbafce9de7daf0b25e26726001ac0..f622c7b5aa497202177be156af5ba9b53d43aa6d 100644
--- a/reviewboard/static/rb/js/common/actions/views/actionView.ts
+++ b/reviewboard/static/rb/js/common/actions/views/actionView.ts
@@ -17,6 +17,7 @@ export class ActionView<
 > extends BaseView<TModel, TElement, TExtraViewOptions> {
     static modelEvents = {
         'change:isQuickAccessEnabled': '_onQuickAccessEnabledChanged',
+        'change:visible': '_onModelVisibleChanged',
     };
 
     /**********************
@@ -31,26 +32,6 @@ export class ActionView<
      */
     #visibilityEl: HTMLElement = null;
 
-    /**
-     * Return whether this action is visible.
-     *
-     * This will return the visibility status of the action parent container
-     * (if available) or of this view's element (if contained in another
-     * parent).
-     *
-     * Version Added:
-     *     7.1
-     *
-     * Returns:
-     *     boolean:
-     *     ``true`` if the action is visible. ``false`` if it is not.
-     */
-    isVisible(): boolean {
-        const el = this.#getVisibilityEl();
-
-        return !el.hidden && el.style.display !== 'none';
-    }
-
     /**
      * Show the action.
      *
@@ -65,10 +46,7 @@ export class ActionView<
      *     This view, for chaining.
      */
     show(): this {
-        const visibilityEl = this.#getVisibilityEl();
-
-        $(visibilityEl).show();
-        visibilityEl.hidden = false;
+        this.model.set('visible', true);
 
         return this;
     }
@@ -87,14 +65,24 @@ export class ActionView<
      *     This view, for chaining.
      */
     hide(): this {
-        const visibilityEl = this.#getVisibilityEl();
-
-        $(visibilityEl).hide();
-        visibilityEl.hidden = true;
+        this.model.set('visible', false);
 
         return this;
     }
 
+    /**
+     * Activate the action.
+     *
+     * By default, this invokes the ``activate`` method on the action model.
+     * This can be overridden to perform different logic.
+     *
+     * Version Added:
+     *     7.1
+     */
+    activate() {
+        this.model.activate();
+    }
+
     /**
      * Handle the initial render of the view.
      *
@@ -111,6 +99,34 @@ export class ActionView<
 
             this._onQuickAccessEnabledChanged();
         }
+
+        this._onModelVisibleChanged();
+    }
+
+    /**
+     * Handle changes to the action's visibility.
+     *
+     * This will update the visibility of the action's view to match the
+     * visibility state, handling both appearance and accessibility.
+     *
+     * Version Added:
+     *     7.1
+     */
+    private _onModelVisibleChanged() {
+        const visibilityEl = this.#getVisibilityEl();
+        const visible = this.model.get('visible');
+
+        /*
+         * The visibility state has changed. Show/hide and update the
+         * hidden attribute.
+         */
+        if (visible) {
+            $(visibilityEl).show();
+            visibilityEl.hidden = false;
+        } else {
+            $(visibilityEl).hide();
+            visibilityEl.hidden = true;
+        }
     }
 
     /**
diff --git a/reviewboard/static/rb/js/common/actions/views/menuActionView.ts b/reviewboard/static/rb/js/common/actions/views/menuActionView.ts
index 7f6c3a8128f9879aca0bb8999fe94d73d49602ed..04ffee49eb464687ed33deadf6ae19027d1f6b06 100644
--- a/reviewboard/static/rb/js/common/actions/views/menuActionView.ts
+++ b/reviewboard/static/rb/js/common/actions/views/menuActionView.ts
@@ -47,6 +47,7 @@ export class MenuActionView<
     protected onInitialRender() {
         const menuItems = new MenuItemsCollection();
         const page = RB.PageManager.getPage();
+        const attachmentPointID = this.attachmentPointID;
 
         for (const childId of this.model.get('children')) {
             if (childId === '--') {
@@ -54,7 +55,8 @@ export class MenuActionView<
                     type: MenuItemType.SEPARATOR,
                 });
             } else {
-                const childActionView = page.getActionView(childId);
+                const childActionView = page.getActionView(childId,
+                                                           attachmentPointID);
 
                 if (childActionView) {
                     const childAction = childActionView.model;
@@ -339,11 +341,4 @@ export class MenuItemActionView extends ActionView {
          */
         this.activate();
     }
-
-    /**
-     * Activate the action.
-     */
-    activate() {
-        // This is expected to be overridden by subclasses.
-    }
 }
diff --git a/reviewboard/static/rb/js/common/models/userSessionModel.ts b/reviewboard/static/rb/js/common/models/userSessionModel.ts
index 9cc610f2ee19c72c316b5b883d161f22d9ca104b..c5b8d946a336d6c286e2f841d9d07a78a928ca53 100644
--- a/reviewboard/static/rb/js/common/models/userSessionModel.ts
+++ b/reviewboard/static/rb/js/common/models/userSessionModel.ts
@@ -131,7 +131,7 @@ interface StoredItemsAttrs extends BaseResourceAttrs {
  * of objects.
  */
 @spina
-class StoredItems extends BaseResource<StoredItemsAttrs> {
+export class StoredItems extends BaseResource<StoredItemsAttrs> {
     /**
      * Return the defaults for the model attributes.
      *
diff --git a/reviewboard/static/rb/js/reviews/index.ts b/reviewboard/static/rb/js/reviews/index.ts
index 5e4873a48485d16fcc4e0b05b7111841567e2f8f..0caeb5ee0abeac5ad715694a7cce0e55bdadc2c4 100644
--- a/reviewboard/static/rb/js/reviews/index.ts
+++ b/reviewboard/static/rb/js/reviews/index.ts
@@ -24,6 +24,7 @@ export {
 } from './models/fileAttachmentReviewableModel';
 export { ImageReviewable } from './models/imageReviewableModel';
 export { RegionCommentBlock } from './models/regionCommentBlockModel';
+export * from './models/reviewRequestActionModels';
 export { ReviewRequestEditor } from './models/reviewRequestEditorModel';
 export { ReviewablePage } from './models/reviewablePageModel';
 export { ScreenshotCommentBlock } from './models/screenshotCommentBlockModel';
diff --git a/reviewboard/static/rb/js/reviews/models/reviewRequestActionModels.ts b/reviewboard/static/rb/js/reviews/models/reviewRequestActionModels.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4d80e217c0506bef4f04cdf5d9fe82225550da79
--- /dev/null
+++ b/reviewboard/static/rb/js/reviews/models/reviewRequestActionModels.ts
@@ -0,0 +1,581 @@
+import {
+    spina,
+} from '@beanbag/spina';
+import {
+    type ButtonView,
+    craft,
+    paint,
+} from '@beanbag/ink';
+
+import {
+    type Review,
+    Actions,
+    ReviewRequest,
+} from 'reviewboard/common';
+import {
+    type StoredItems,
+    UserSession,
+} from 'reviewboard/common/models/userSessionModel';
+import {
+    type ReviewRequestEditorView,
+} from '../views/reviewRequestEditorView';
+import { ReviewDialogView } from '../views/reviewDialogView';
+
+
+/**
+ * Base class for archive actions.
+ *
+ * Version Added:
+ *     7.1:
+ *     This implements the action logic formerly found in
+ *     :js:class:`RB.BaseVisibilityActionView`.
+ */
+@spina
+class BaseVisibilityAction extends Actions.Action {
+    /**********************
+     * Instance variables *
+     **********************/
+
+    /** The collection to use for making changes to the visibility. */
+    targetCollection: StoredItems;
+
+    /** The visibility type controlled by this action. */
+    visibilityType = ReviewRequest.VISIBILITY_ARCHIVED;
+
+    /**
+     * Initialize the view.
+     */
+    initialize() {
+        super.initialize();
+
+        const page = RB.PageManager.getPage();
+        const reviewRequestEditor = page.getReviewRequestEditorModel();
+        const reviewRequest = reviewRequestEditor.get('reviewRequest');
+
+        this.listenTo(reviewRequest, 'change:visibility',
+                      this.#onReviewRequestVisibilityChanged);
+        this.#onReviewRequestVisibilityChanged(
+            reviewRequest,
+            reviewRequest.get('visibility'));
+    }
+
+    /**
+     * Return the label to use for the action.
+     *
+     * Args:
+     *     visibility (number):
+     *         The visibility state of the review request.
+     *
+     * Returns:
+     *     string:
+     *     The text to use for the label.
+     */
+    getLabelForVisibility(
+        visibility: number,
+    ): string {
+        console.assert(
+            false,
+            `getLabelsForVisibility must be implemented in ${this}`,
+        );
+
+        return null;
+    }
+
+    /**
+     * Toggle the archive state of the review request.
+     */
+    async activate() {
+        const page = RB.PageManager.getPage();
+        const reviewRequestEditor = page.getReviewRequestEditorModel();
+        const reviewRequest = reviewRequestEditor.get('reviewRequest');
+
+        const visibility = reviewRequest.get('visibility');
+        const visible = (visibility !== this.visibilityType);
+
+        if (visible) {
+            await this.targetCollection.addImmediately(reviewRequest);
+        } else {
+            await this.targetCollection.removeImmediately(reviewRequest);
+        }
+
+        reviewRequest.set('visibility',
+                          visible
+                          ? this.visibilityType
+                          : ReviewRequest.VISIBILITY_VISIBLE);
+    }
+
+    /**
+     * Handle changes to the review request's visibility.
+     *
+     * This will update the label based on the review request's visibility
+     * state.
+     *
+     * Args:
+     *     model (RB.ReviewRequest):
+     *         The review request being managed.
+     *
+     *     visibility (number):
+     *         The review request's visibility.
+     */
+    #onReviewRequestVisibilityChanged(
+        model: ReviewRequest,
+        visibility: number,
+    ) {
+        this.set('label', this.getLabelForVisibility(visibility));
+    }
+}
+
+
+/**
+ * Archive action.
+ *
+ * Version Added:
+ *     7.1:
+ *     This implements the action logic formerly found in
+ *     :js:class:`RB.ArchiveActionView`.
+ */
+@spina
+export class ArchiveAction extends BaseVisibilityAction {
+    /**********************
+     * Instance variables *
+     **********************/
+
+    /** The collection to use for making changes to the visibility. */
+    targetCollection = UserSession.instance.archivedReviewRequests;
+
+    /**
+     * Return the label to use for the action.
+     *
+     * Args:
+     *     visibility (number):
+     *         The visibility state of the review request.
+     *
+     * Returns:
+     *     string:
+     *     The text to use for the label.
+     */
+    getLabelForVisibility(
+        visibility: number,
+    ): string {
+        return visibility === this.visibilityType
+               ? _`Unarchive`
+               : _`Archive`;
+    }
+}
+
+
+/**
+ * Mute action.
+ *
+ * Version Added:
+ *     7.1:
+ *     This implements the action logic formerly found in
+ *     :js:class:`RB.MuteActionView`.
+ */
+@spina
+export class MuteAction extends BaseVisibilityAction {
+    /**********************
+     * Instance variables *
+     **********************/
+
+    /** The collection to use for making changes to the visibility. */
+    targetCollection = UserSession.instance.mutedReviewRequests;
+
+    /** The visibility type controlled by this action. */
+    visibilityType = ReviewRequest.VISIBILITY_MUTED;
+
+    /**
+     * Return the label to use for the menu item.
+     *
+     * Args:
+     *     visibility (number):
+     *         The visibility state of the review request.
+     *
+     * Returns:
+     *     string:
+     *     The text to use for the label.
+     */
+    getLabelForVisibility(
+        visibility: number,
+    ): string {
+        return visibility === this.visibilityType
+               ? _`Unmute`
+               : _`Mute`;
+    }
+}
+
+
+/**
+ * Action to create a blank review.
+ *
+ * Version Added:
+ *     7.1:
+ *     This implements the action logic formerly found in
+ *     :js:class:`RB.CreateReviewActionView`.
+ */
+@spina
+export class CreateReviewAction extends Actions.Action {
+    /**********************
+     * Instance variables *
+     **********************/
+
+    /** The pending review to create or manage. */
+    #pendingReview: Review;
+
+    /**
+     * Initialize the action.
+     */
+    initialize() {
+        super.initialize();
+
+        const page = RB.PageManager.getPage();
+        this.#pendingReview = page.pendingReview;
+
+        this.listenTo(this.#pendingReview, 'saved destroy sync', this.#update);
+        this.#update();
+    }
+
+    /**
+     * Update the visibility state of the action.
+     *
+     * This will show the action only when there's no existing pending review.
+     */
+    #update() {
+        this.set('visible', this.#pendingReview.isNew());
+    }
+
+    /**
+     * Handle activation of the action.
+     */
+    activate() {
+        this.#pendingReview.save()
+            .then(() => {
+                const page = RB.PageManager.getPage();
+
+                ReviewDialogView.create({
+                    review: this.#pendingReview,
+                    reviewRequestEditor: page.getReviewRequestEditorModel(),
+                });
+            });
+    }
+}
+
+
+/**
+ * Action to pop up the Edit Review dialog.
+ *
+ * Version Added:
+ *     7.1:
+ *     This implements the action logic formerly found in
+ *     :js:class:`RB.EditReviewActionView`.
+ */
+@spina
+export class EditReviewAction extends Actions.Action {
+    /**********************
+     * Instance variables *
+     **********************/
+
+    /** The pending review to create or manage. */
+    #pendingReview: Review;
+
+    /**
+     * Initialize the action.
+     */
+    initialize() {
+        super.initialize();
+
+        const page = RB.PageManager.getPage();
+        this.#pendingReview = page.pendingReview;
+
+        this.listenTo(this.#pendingReview, 'saved destroy sync', this.#update);
+        this.#update();
+    }
+
+    /**
+     * Update the visibility state of the action.
+     *
+     * This will show the action only when there's no existing pending review.
+     */
+    #update() {
+        this.set('visible', !this.#pendingReview.isNew());
+    }
+
+    /**
+     * Handle activation of the action.
+     */
+    activate() {
+        const page = RB.PageManager.getPage();
+
+        ReviewDialogView.create({
+            review: this.#pendingReview,
+            reviewRequestEditor: page.getReviewRequestEditorModel(),
+        });
+    }
+}
+
+
+/**
+ * Action to add a general comment.
+ *
+ * Version Added:
+ *     7.1:
+ *     This implements the action logic formerly found in
+ *     :js:class:`RB.AddGeneralCommentActionView`.
+ */
+@spina
+export class AddGeneralCommentAction extends Actions.Action {
+    /**
+     * Handle the action activation.
+     */
+    activate() {
+        RB.PageManager.getPage().addGeneralComment();
+    }
+}
+
+
+/**
+ * Action view to mark a review request as "Ship It".
+ *
+ * Version Added:
+ *     7.1:
+ *     This implements the action logic formerly found in
+ *     :js:class:`RB.ShipItActionView`.
+ */
+@spina
+export class ShipItAction extends Actions.Action {
+    /**
+     * Handle the action activation.
+     */
+    activate() {
+        RB.PageManager.getPage().shipIt();
+    }
+}
+
+
+/**
+ * Action for the "Add File" command.
+ *
+ * Version Added:
+ *     7.1:
+ *     This implements the action logic formerly found in
+ *     :js:class:`RB.AddFileActionView`.
+ */
+@spina
+export class AddFileAction extends Actions.Action {
+    /**
+     * Handle the action activation.
+     */
+    activate() {
+        const page = RB.PageManager.getPage();
+        const reviewRequestEditorView = page.reviewRequestEditorView as
+            ReviewRequestEditorView;
+        const reviewRequestEditor = reviewRequestEditorView.model;
+
+        if (reviewRequestEditor.hasUnviewedUserDraft) {
+            reviewRequestEditorView.promptToLoadUserDraft();
+        } else {
+            const uploadDialog = new RB.UploadAttachmentView({
+                reviewRequestEditor: reviewRequestEditor,
+            });
+            uploadDialog.show();
+        }
+    }
+}
+
+
+/**
+ * Action for the "Update Diff" command.
+ *
+ * Version Added:
+ *     7.1:
+ *     This implements the action logic formerly found in
+ *     :js:class:`RB.UpdateDiffAction`.
+ */
+@spina
+export class UpdateDiffAction extends Actions.Action {
+    /**
+     * Handle the action activation.
+     */
+    activate() {
+        const page = RB.PageManager.getPage();
+        const reviewRequestEditorView = page.reviewRequestEditorView as
+            ReviewRequestEditorView;
+        const reviewRequestEditor = reviewRequestEditorView.model;
+        const reviewRequest = reviewRequestEditor.get('reviewRequest');
+
+        if (reviewRequestEditor.hasUnviewedUserDraft) {
+            reviewRequestEditorView.promptToLoadUserDraft();
+        } else if (reviewRequestEditor.get('commits').length > 0) {
+            const rbtoolsURL = 'https://www.reviewboard.org/docs/rbtools/latest/';
+
+            const $dialog = $('<div>')
+                .append($('<p>')
+                    .html(_`
+                        This review request was created with
+                        <a href="${rbtoolsURL}">RBTools</a>,
+                        and is tracking commit history.
+                    `))
+                .append($('<p>')
+                    .html(_`
+                        To add a new diff revision, you will need to use
+                        <code>rbt post -u</code> instead of uploading a diff
+                        file.
+                    `))
+                .modalBox({
+                    buttons: [
+                        paint<HTMLButtonElement>`
+                            <Ink.Button>${_`Cancel`}</Ink.Button>
+                        `,
+                    ],
+                    title: _`Use RBTools to update the diff`,
+                })
+                .on('close', () => {
+                    $dialog.modalBox('destroy');
+                });
+        } else {
+            const updateDiffView = new RB.UpdateDiffView({
+                model: new RB.UploadDiffModel({
+                    changeNumber: reviewRequest.get('commitID'),
+                    repository: reviewRequest.get('repository'),
+                    reviewRequest: reviewRequest,
+                }),
+            });
+            updateDiffView.render();
+        }
+    }
+}
+
+
+/**
+ * Action for the "Close > Discarded" command.
+ *
+ * Version Added:
+ *     7.1:
+ *     This implements the action logic formerly found in
+ *     :js:class:`RB.CloseDiscardedActionView`.
+ */
+@spina
+export class CloseDiscardedAction extends Actions.Action {
+    /**
+     * Handle the action activation.
+     */
+    activate() {
+        const page = RB.PageManager.getPage();
+        const reviewRequestEditorView = page.reviewRequestEditorView as
+            ReviewRequestEditorView;
+        const reviewRequestEditor = reviewRequestEditorView.model;
+        const reviewRequest = reviewRequestEditor.get('reviewRequest');
+
+        const confirmText =
+            _`Are you sure you want to discard this review request?`;
+
+        if (confirm(confirmText)) {
+            reviewRequest
+                .close({
+                    type: ReviewRequest.CLOSE_DISCARDED,
+                })
+                .catch(err => this.trigger('closeError', err.message));
+        }
+    }
+}
+
+
+/**
+ * Action for the "Close > Completed" command.
+ *
+ * Version Added:
+ *     7.1:
+ *     This implements the action logic formerly found in
+ *     :js:class:`RB.CloseCompletedActionView`.
+ */
+@spina
+export class CloseCompletedAction extends Actions.Action {
+    /**
+     * Handle the action activation.
+     */
+    activate() {
+        const page = RB.PageManager.getPage();
+        const reviewRequestEditorView = page.reviewRequestEditorView as
+            ReviewRequestEditorView;
+        const reviewRequestEditor = reviewRequestEditorView.model;
+        const reviewRequest = reviewRequestEditor.get('reviewRequest');
+
+        /*
+         * This is a non-destructive event, so don't confirm unless there's
+         * a draft.
+         */
+        let submit = true;
+
+        if (reviewRequestEditor.get('hasDraft')) {
+            submit = confirm(_`
+                You have an unpublished draft. If you close this review
+                request, the draft will be discarded. Are you sure you want
+                to close the review request?
+            `);
+        }
+
+        if (submit) {
+            reviewRequest
+                .close({
+                    type: ReviewRequest.CLOSE_SUBMITTED,
+                })
+                .catch(err => this.trigger('closeError', err.message));
+        }
+    }
+}
+
+
+/**
+ * Action for the "Close > Delete Permanently" command.
+ *
+ * Version Added:
+ *     7.1:
+ *     This implements the action logic formerly found in
+ *     :js:class:`RB.DeleteActionView`.
+ */
+@spina
+export class DeleteAction extends Actions.Action {
+    /**
+     * Handle the action activation.
+     */
+    activate() {
+        const page = RB.PageManager.getPage();
+        const reviewRequestEditorView = page.reviewRequestEditorView as
+            ReviewRequestEditorView;
+        const reviewRequestEditor = reviewRequestEditorView.model;
+        const reviewRequest = reviewRequestEditor.get('reviewRequest');
+
+        const onDeleteConfirmed = () => {
+            deleteButtonView.busy = true;
+            reviewRequest
+                .destroy({
+                    buttons: buttonEls,
+                })
+            .then(() => RB.navigateTo(SITE_ROOT));
+        };
+
+        const deleteButtonView = craft<ButtonView>`
+            <Ink.Button type="danger" onClick=${onDeleteConfirmed}>
+             ${_`Delete`}
+            </Ink.Button>
+        `;
+
+        const buttonEls = paint<HTMLButtonElement[]>`
+            <Ink.Button>
+             ${_`Cancel`}
+            </Ink.Button>
+            ${deleteButtonView.el}
+        `;
+
+        const $dlg = $('<p>')
+            .text(_`
+                This deletion cannot be undone. All diffs and reviews will be
+                deleted as well.
+            `)
+            .modalBox({
+                buttons: buttonEls,
+                title: _`Are you sure you want to delete this review request?`,
+            })
+            .on('close', () => $dlg.modalBox('destroy'));
+    }
+}
diff --git a/reviewboard/static/rb/js/reviews/views/reviewRequestActions.ts b/reviewboard/static/rb/js/reviews/views/reviewRequestActions.ts
index 2e1d9292240c2a60879ddf57d758b3d30850d4c0..f169308df8dbf0c3f1f3b1ce688b121b0e49945b 100644
--- a/reviewboard/static/rb/js/reviews/views/reviewRequestActions.ts
+++ b/reviewboard/static/rb/js/reviews/views/reviewRequestActions.ts
@@ -1,26 +1,15 @@
-import {
-    type ButtonView,
-    craft,
-    paint,
-} from '@beanbag/ink';
 import {
     type EventsHash,
     spina,
 } from '@beanbag/spina';
 
 import {
-    type BaseResource,
-    type Review,
     Actions,
     ReviewRequest,
     UserSession,
 } from 'reviewboard/common';
 import { OverlayView } from 'reviewboard/ui';
 
-import { type ReviewRequestEditor } from '../models/reviewRequestEditorModel';
-import { type ReviewRequestEditorView } from './reviewRequestEditorView';
-import { ReviewDialogView } from './reviewDialogView';
-
 
 /**
  * The view to manage the archive menu.
@@ -171,335 +160,6 @@ export class ArchiveMenuActionView extends Actions.MenuActionView {
 }
 
 
-/**
- * Base class for archive views.
- *
- * Version Added:
- *     6.0
- */
-@spina
-class BaseVisibilityActionView extends Actions.MenuItemActionView {
-    /**********************
-     * Instance variables *
-     **********************/
-
-    /** The collection to use for making changes to the visibility. */
-    collection: BaseResource;
-
-    /** The visibility type controlled by this action. */
-    visibilityType = ReviewRequest.VISIBILITY_ARCHIVED;
-
-    #reviewRequest: ReviewRequest;
-
-    /**
-     * Initialize the view.
-     */
-    initialize() {
-        super.initialize();
-
-        const page = RB.PageManager.getPage();
-        const reviewRequestEditor = page.getReviewRequestEditorModel();
-        this.#reviewRequest = reviewRequestEditor.get('reviewRequest');
-
-        this.listenTo(this.#reviewRequest, 'change:visibility', this.render);
-    }
-
-    /**
-     * Render the view.
-     *
-     * Returns:
-     *     BaseVisibilityActionView:
-     *     This object, for chaining.
-     */
-    protected onRender() {
-        this.model.set('label',
-                       this.getLabel(this.#reviewRequest.get('visibility')));
-    }
-
-    /**
-     * Return the label to use for the menu item.
-     *
-     * Args:
-     *     visibility (number):
-     *         The visibility state of the review request.
-     *
-     * Returns:
-     *     string:
-     *     The label to show based on the current visibility state.
-     */
-    getLabel(
-        visibility: number,
-    ): string {
-        console.assert(false, 'Not reached.');
-
-        return null;
-    }
-
-    /**
-     * Toggle the archive state of the review request.
-     */
-    async activate() {
-        const visibility = this.#reviewRequest.get('visibility');
-        const visible = (visibility !== this.visibilityType);
-
-        if (visible) {
-            await this.collection.addImmediately(this.#reviewRequest);
-        } else {
-            await this.collection.removeImmediately(this.#reviewRequest);
-        }
-
-        this.#reviewRequest.set('visibility',
-                                visible
-                                ? this.visibilityType
-                                : ReviewRequest.VISIBILITY_VISIBLE);
-    }
-}
-
-
-/**
- * Archive action view.
- *
- * Version Added:
- *     6.0
- */
-@spina
-export class ArchiveActionView extends BaseVisibilityActionView {
-    /**********************
-     * Instance variables *
-     **********************/
-
-    /** The collection to use for making changes to the visibility. */
-    collection = UserSession.instance.archivedReviewRequests;
-
-    /**
-     * Return the label to use for the menu item.
-     *
-     * Args:
-     *     visibility (number):
-     *         The visibility state of the review request.
-     *
-     * Returns:
-     *     string:
-     *     The text to use for the label.
-     */
-    getLabel(
-        visibility: number,
-    ): string {
-        return visibility === this.visibilityType
-               ? _`Unarchive`
-               : _`Archive`;
-    }
-}
-
-
-/**
- * Mute action view.
- *
- * Version Added:
- *     6.0
- */
-@spina
-export class MuteActionView extends BaseVisibilityActionView {
-    /**********************
-     * Instance variables *
-     **********************/
-
-    /** The collection to use for making changes to the visibility. */
-    collection = UserSession.instance.mutedReviewRequests;
-
-    /** The visibility type controlled by this action. */
-    visibilityType = ReviewRequest.VISIBILITY_MUTED;
-
-    /**
-     * Return the label to use for the menu item.
-     *
-     * Args:
-     *     visibility (number):
-     *         The visibility state of the review request.
-     *
-     * Returns:
-     *     string:
-     *     The text to use for the label.
-     */
-    getLabel(
-        visibility: number,
-    ): string {
-        return visibility === this.visibilityType
-               ? _`Unmute`
-               : _`Mute`;
-    }
-}
-
-
-/**
- * Action view to create a blank review.
- *
- * Version Added:
- *     6.0
- */
-@spina
-export class CreateReviewActionView extends Actions.MenuItemActionView {
-    /**********************
-     * Instance variables *
-     **********************/
-
-    #pendingReview: Review;
-    #reviewRequestEditor: ReviewRequestEditor;
-
-    /**
-     * Initialize the view.
-     *
-     * Args:
-     *     options (object):
-     *         Options to pass through to the parent class.
-     */
-    initialize(options: object) {
-        super.initialize(options);
-
-        const page = RB.PageManager.getPage();
-        this.#pendingReview = page.pendingReview;
-        this.#reviewRequestEditor = page.reviewRequestEditorView.model;
-    }
-
-    /**
-     * Render the action.
-     *
-     * Returns:
-     *     CreateReviewActionView:
-     *     This object, for chaining.
-     */
-    protected onInitialRender() {
-        super.onInitialRender();
-
-        this.listenTo(this.#pendingReview, 'saved destroy sync', this.#update);
-        this.#update();
-    }
-
-    /**
-     * Update the visibility state of the action.
-     *
-     * This will show the action only when there's no existing pending review.
-     */
-    #update() {
-        if (this.#pendingReview.isNew()) {
-            this.show();
-        } else {
-            this.hide();
-        }
-    }
-
-    /**
-     * Handle activation of the action.
-     */
-    activate() {
-        this.#pendingReview.save()
-            .then(() => {
-                ReviewDialogView.create({
-                    review: this.#pendingReview,
-                    reviewRequestEditor: this.#reviewRequestEditor,
-                });
-            });
-    }
-}
-
-
-/**
- * Action view to pop up the edit review dialog.
- *
- * Version Added:
- *     6.0
- */
-@spina
-export class EditReviewActionView extends Actions.MenuItemActionView {
-    /**********************
-     * Instance variables *
-     **********************/
-
-    #pendingReview: Review;
-    #reviewRequestEditor: ReviewRequestEditor;
-
-    /**
-     * Create the view.
-     *
-     * Args:
-     *     options (object):
-     *         Options to pass through to the parent class.
-     */
-    initialize(options: object) {
-        super.initialize(options);
-
-        const page = RB.PageManager.getPage();
-        this.#pendingReview = page.pendingReview;
-        this.#reviewRequestEditor = page.reviewRequestEditorView.model;
-    }
-
-    /**
-     * Render the action.
-     */
-    protected onInitialRender() {
-        super.onInitialRender();
-
-        this.listenTo(this.#pendingReview, 'saved destroy sync', this.#update);
-        this.#update();
-    }
-
-    /**
-     * Update the visibility state of the action.
-     */
-    #update() {
-        if (this.#pendingReview.isNew()) {
-            this.hide();
-        } else {
-            this.show();
-        }
-    }
-
-    /**
-     * Handle the action activation.
-     */
-    activate() {
-        ReviewDialogView.create({
-            review: this.#pendingReview,
-            reviewRequestEditor: this.#reviewRequestEditor,
-        });
-    }
-}
-
-
-/**
- * Action view to add a general comment.
- *
- * Version Added:
- *     6.0
- */
-@spina
-export class AddGeneralCommentActionView extends Actions.MenuItemActionView {
-    /**
-     * Handle the action activation.
-     */
-    activate() {
-        RB.PageManager.getPage().addGeneralComment();
-    }
-}
-
-
-/**
- * Action view to mark a review request as "Ship It".
- *
- * Version Added:
- *     6.0
- */
-@spina
-export class ShipItActionView extends Actions.MenuItemActionView {
-    /**
-     * Handle the action activation.
-     */
-    activate() {
-        RB.PageManager.getPage().shipIt();
-    }
-}
-
-
 /**
  * Action view for the review menu.
  *
@@ -591,222 +251,3 @@ export class ReviewMenuActionView extends Actions.MenuActionView {
         }
     }
 }
-
-
-/**
- * Action view for the "Add File" command.
- *
- * Version Added:
- *     6.0
- */
-@spina
-export class AddFileActionView extends Actions.MenuItemActionView {
-    /**
-     * Handle the action activation.
-     */
-    activate() {
-        const page = RB.PageManager.getPage();
-        const reviewRequestEditorView = page.reviewRequestEditorView as
-            ReviewRequestEditorView;
-        const reviewRequestEditor = reviewRequestEditorView.model;
-
-        if (reviewRequestEditor.hasUnviewedUserDraft) {
-            reviewRequestEditorView.promptToLoadUserDraft();
-        } else {
-            const uploadDialog = new RB.UploadAttachmentView({
-                reviewRequestEditor: reviewRequestEditor,
-            });
-            uploadDialog.show();
-        }
-    }
-}
-
-
-/**
- * Action view for the "Update Diff" command.
- *
- * Version Added:
- *     6.0
- */
-@spina
-export class UpdateDiffActionView extends Actions.MenuItemActionView {
-    /**
-     * Handle the action activation.
-     */
-    activate() {
-        const page = RB.PageManager.getPage();
-        const reviewRequestEditorView = page.reviewRequestEditorView as
-            ReviewRequestEditorView;
-        const reviewRequestEditor = reviewRequestEditorView.model;
-        const reviewRequest = reviewRequestEditor.get('reviewRequest');
-
-        if (reviewRequestEditor.hasUnviewedUserDraft) {
-            reviewRequestEditorView.promptToLoadUserDraft();
-        } else if (reviewRequestEditor.get('commits').length > 0) {
-            const rbtoolsURL = 'https://www.reviewboard.org/docs/rbtools/latest/';
-
-            const $dialog = $('<div>')
-                .append($('<p>')
-                    .html(_`
-                        This review request was created with
-                        <a href="${rbtoolsURL}">RBTools</a>,
-                        and is tracking commit history.
-                    `))
-                .append($('<p>')
-                    .html(_`
-                        To add a new diff revision, you will need to use
-                        <code>rbt post -u</code> instead of uploading a diff
-                        file.
-                    `))
-                .modalBox({
-                    buttons: [
-                        paint<HTMLButtonElement>`
-                            <Ink.Button>${_`Cancel`}</Ink.Button>
-                        `,
-                    ],
-                    title: _`Use RBTools to update the diff`,
-                })
-                .on('close', () => {
-                    $dialog.modalBox('destroy');
-                });
-        } else {
-            const updateDiffView = new RB.UpdateDiffView({
-                model: new RB.UploadDiffModel({
-                    changeNumber: reviewRequest.get('commitID'),
-                    repository: reviewRequest.get('repository'),
-                    reviewRequest: reviewRequest,
-                }),
-            });
-            updateDiffView.render();
-        }
-    }
-}
-
-
-/**
- * Action view for the "Close > Discarded" command.
- *
- * Version Added:
- *     6.0
- */
-@spina
-export class CloseDiscardedActionView extends Actions.MenuItemActionView {
-    /**
-     * Handle the action activation.
-     */
-    activate() {
-        const page = RB.PageManager.getPage();
-        const reviewRequestEditorView = page.reviewRequestEditorView as
-            ReviewRequestEditorView;
-        const reviewRequestEditor = reviewRequestEditorView.model;
-        const reviewRequest = reviewRequestEditor.get('reviewRequest');
-
-        const confirmText =
-            _`Are you sure you want to discard this review request?`;
-
-        if (confirm(confirmText)) {
-            reviewRequest
-                .close({
-                    type: ReviewRequest.CLOSE_DISCARDED,
-                })
-                .catch(err => this.model.trigger('closeError', err.message));
-        }
-    }
-}
-
-
-/**
- * Action view for the "Close > Completed" command.
- *
- * Version Added:
- *     6.0
- */
-@spina
-export class CloseCompletedActionView extends Actions.MenuItemActionView {
-    /**
-     * Handle the action activation.
-     */
-    activate() {
-        const page = RB.PageManager.getPage();
-        const reviewRequestEditorView = page.reviewRequestEditorView as
-            ReviewRequestEditorView;
-        const reviewRequestEditor = reviewRequestEditorView.model;
-        const reviewRequest = reviewRequestEditor.get('reviewRequest');
-
-        /*
-         * This is a non-destructive event, so don't confirm unless there's
-         * a draft.
-         */
-        let submit = true;
-
-        if (reviewRequestEditor.get('hasDraft')) {
-            submit = confirm(_`
-                You have an unpublished draft. If you close this review
-                request, the draft will be discarded. Are you sure you want
-                to close the review request?
-            `);
-        }
-
-        if (submit) {
-            reviewRequest
-                .close({
-                    type: ReviewRequest.CLOSE_SUBMITTED,
-                })
-                .catch(err => this.model.trigger('closeError', err.message));
-        }
-    }
-}
-
-
-/**
- * Action view for the "Close > Delete Permanently" command.
- *
- * Version Added:
- *     6.0
- */
-@spina
-export class DeleteActionView extends Actions.MenuItemActionView {
-    /**
-     * Handle the action activation.
-     */
-    activate() {
-        const page = RB.PageManager.getPage();
-        const reviewRequestEditorView = page.reviewRequestEditorView as
-            ReviewRequestEditorView;
-        const reviewRequestEditor = reviewRequestEditorView.model;
-        const reviewRequest = reviewRequestEditor.get('reviewRequest');
-
-        const onDeleteConfirmed = () => {
-            deleteButtonView.busy = true;
-            reviewRequest
-                .destroy({
-                    buttons: buttonEls,
-                })
-            .then(() => RB.navigateTo(SITE_ROOT));
-        };
-
-        const deleteButtonView = craft<ButtonView>`
-            <Ink.Button type="danger" onClick=${onDeleteConfirmed}>
-             ${_`Delete`}
-            </Ink.Button>
-        `;
-
-        const buttonEls = paint<HTMLButtonElement[]>`
-            <Ink.Button>
-             ${_`Cancel`}
-            </Ink.Button>
-            ${deleteButtonView.el}
-        `;
-
-        const $dlg = $('<p>')
-            .text(_`
-                This deletion cannot be undone. All diffs and reviews will be
-                deleted as well.
-            `)
-            .modalBox({
-                buttons: buttonEls,
-                title: _`Are you sure you want to delete this review request?`,
-            })
-            .on('close', () => $dlg.modalBox('destroy'));
-    }
-}
