FIX: reliably reorder link in custom sections (#24188)

Two changes were introduced:
1. Reorder links on sidebar section is removed. Clicking and holding the mouse for 250ms was unintuitive;
2. Fixed bugs when reorder is done in edit modal.
This commit is contained in:
Krzysztof Kotlarek 2023-11-02 08:46:45 +11:00 committed by GitHub
parent 1d96b0a99a
commit 1c395e1a01
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 34 additions and 232 deletions

View File

@ -51,6 +51,7 @@
@link={{link}}
@deleteLink={{this.deleteLink}}
@reorderCallback={{this.reorder}}
@setDraggedLinkCallback={{this.setDraggedLink}}
/>
{{/each}}
@ -71,6 +72,7 @@
@link={{link}}
@deleteLink={{this.deleteLink}}
@reorderCallback={{this.reorder}}
@setDraggedLinkCallback={{this.setDraggedLink}}
/>
{{/each}}
<DButton

View File

@ -384,38 +384,36 @@ export default class SidebarSectionForm extends Component {
}
@bind
reorder(linkFromId, linkTo, above) {
if (linkFromId === linkTo.objectId) {
setDraggedLink(link) {
this.draggedLink = link;
}
@bind
reorder(targetLink, above) {
if (this.draggedLink === targetLink) {
return;
}
let linkFrom = this.transformedModel.links.find(
(link) => link.objectId === linkFromId
);
if (!linkFrom) {
linkFrom = this.transformedModel.secondaryLinks.find(
(link) => link.objectId === linkFromId
);
}
if (linkFrom.isPrimary) {
this.transformedModel.links.removeObject(linkFrom);
if (this.draggedLink.isPrimary) {
this.transformedModel.links.removeObject(this.draggedLink);
} else {
this.transformedModel.secondaryLinks?.removeObject(linkFrom);
this.transformedModel.secondaryLinks?.removeObject(this.draggedLink);
}
if (linkTo.isPrimary) {
const toPosition = this.transformedModel.links.indexOf(linkTo);
linkFrom.segment = "primary";
if (targetLink.isPrimary) {
const toPosition = this.transformedModel.links.indexOf(targetLink);
this.draggedLink.segment = "primary";
this.transformedModel.links.insertAt(
above ? toPosition : toPosition + 1,
linkFrom
this.draggedLink
);
} else {
linkFrom.segment = "secondary";
const toPosition = this.transformedModel.secondaryLinks.indexOf(linkTo);
this.draggedLink.segment = "secondary";
const toPosition =
this.transformedModel.secondaryLinks.indexOf(targetLink);
this.transformedModel.secondaryLinks.insertAt(
above ? toPosition : toPosition + 1,
linkFrom
this.draggedLink
);
}
}

View File

@ -19,15 +19,6 @@
@fullReload={{link.fullReload}}
@href={{link.value}}
@class={{link.linkDragCss}}
{{(if
this.section.reorderable
(modifier
"draggable"
didStartDrag=link.didStartDrag
didEndDrag=link.didEndDrag
dragMove=link.dragMove
)
)}}
/>
{{else}}
<Sidebar::SectionLink
@ -48,15 +39,6 @@
@suffixType={{link.suffixType}}
@currentWhen={{link.currentWhen}}
@class={{link.linkDragCss}}
{{(if
this.section.reorderable
(modifier
"draggable"
didStartDrag=link.didStartDrag
didEndDrag=link.didEndDrag
dragMove=link.dragMove
)
)}}
/>
{{/if}}
{{/each}}

View File

@ -1,6 +1,7 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { action } from "@ember/object";
import discourseLater from "discourse-common/lib/later";
export default class SectionFormLink extends Component {
@tracked dragCssClass;
@ -17,14 +18,14 @@ export default class SectionFormLink extends Component {
@action
dragHasStarted(event) {
event.dataTransfer.effectAllowed = "move";
event.dataTransfer.setData("linkId", this.args.link.objectId);
this.args.setDraggedLinkCallback(this.args.link);
this.dragCssClass = "dragging";
}
@action
dragOver(event) {
event.preventDefault();
if (!this.dragCssClass) {
if (this.dragCssClass !== "dragging") {
if (this.isAboveElement(event)) {
this.dragCssClass = "drag-above";
} else {
@ -44,25 +45,23 @@ export default class SectionFormLink extends Component {
this.dragCount === 0 &&
(this.dragCssClass === "drag-above" || this.dragCssClass === "drag-below")
) {
this.dragCssClass = null;
discourseLater(() => {
this.dragCssClass = null;
}, 10);
}
}
@action
dropItem(event) {
event.stopPropagation();
this.dragCounter = 0;
this.args.reorderCallback(
parseInt(event.dataTransfer.getData("linkId"), 10),
this.args.link,
this.isAboveElement(event)
);
this.dragCount = 0;
this.args.reorderCallback(this.args.link, this.isAboveElement(event));
this.dragCssClass = null;
}
@action
dragEnd() {
this.dragCounter = 0;
this.dragCount = 0;
this.dragCssClass = null;
}
}

View File

@ -41,7 +41,6 @@ export default class CommunitySection {
@tracked links;
@tracked moreLinks;
reorderable = false;
hideSectionHeader = true;
constructor({ section, owner }) {

View File

@ -1,11 +1,6 @@
import { tracked } from "@glimmer/tracking";
import RouteInfoHelper from "discourse/lib/sidebar/route-info-helper";
import { defaultHomepage } from "discourse/lib/utilities";
import discourseLater from "discourse-common/lib/later";
import { bind } from "discourse-common/utils/decorators";
const TOUCH_SCREEN_DELAY = 300;
const MOUSE_DELAY = 250;
export default class SectionLink {
@tracked linkDragCss;
@ -46,100 +41,4 @@ export default class SectionLink {
get externalOrFullReload() {
return this.external || this.fullReload || this.withAnchor;
}
@bind
didStartDrag(event) {
// 0 represents left button of the mouse
if (event.button === 0 || event.targetTouches) {
this.startMouseY = this.#calcMouseY(event);
this.willDrag = true;
discourseLater(
() => {
this.delayedStart(event);
},
event.targetTouches ? TOUCH_SCREEN_DELAY : MOUSE_DELAY
);
}
}
delayedStart(event) {
if (this.willDrag) {
const currentMouseY = this.#calcMouseY(event);
if (currentMouseY === this.startMouseY) {
event.stopPropagation();
event.preventDefault();
this.mouseY = this.#calcMouseY(event);
this.linkDragCss = "drag";
this.section.disable();
this.drag = true;
}
}
}
@bind
didEndDrag() {
this.linkDragCss = null;
this.mouseY = null;
this.section.enable();
this.section.reorder();
this.willDrag = false;
this.drag = false;
}
@bind
dragMove(event) {
const moveMouseY = this.#calcMouseY(event);
if (this.willDrag && moveMouseY !== this.startMouseY && !this.drag) {
/**
* If mouse position is different, it means that it is a scroll and not drag and drop action.
* In that case, we want to do nothing and keep original behaviour.
*/
this.willDrag = false;
return;
} else {
/**
* Otherwise, event propagation should be stopped as we have our own handler for drag and drop.
*/
event.stopPropagation();
event.preventDefault();
}
this.startMouseY = moveMouseY;
if (!this.drag) {
return;
}
const currentMouseY = this.#calcMouseY(event);
const distance = currentMouseY - this.mouseY;
if (!this.linkHeight) {
this.linkHeight = document.getElementsByClassName(
"sidebar-section-link-wrapper"
)[0].clientHeight;
}
if (distance >= this.linkHeight) {
if (this.section.links.indexOf(this) !== this.section.links.length - 1) {
this.section.moveLinkDown(this);
this.mouseY = currentMouseY;
}
}
if (distance <= -this.linkHeight) {
if (this.section.links.indexOf(this) !== 0) {
this.section.moveLinkUp(this);
this.mouseY = currentMouseY;
}
}
}
#calcMouseY(event) {
return Math.round(
event.targetTouches ? event.targetTouches[0].clientY : event.y
);
}
}

View File

@ -15,8 +15,6 @@ export default class Section {
@tracked dragCss;
@tracked links;
reorderable = true;
constructor({ section, owner }) {
setOwner(this, owner);

View File

@ -83,17 +83,6 @@ class SidebarSectionsController < ApplicationController
render_serialized(sidebar_section, SidebarSectionSerializer)
end
def reorder
sidebar_section = SidebarSection.find_by(id: reorder_params["sidebar_section_id"])
@guardian.ensure_can_edit!(sidebar_section)
order = reorder_params["links_order"].map(&:to_i).each_with_index.to_h
set_order(sidebar_section, order)
render_serialized(sidebar_section, SidebarSectionSerializer)
rescue Discourse::InvalidAccess
render json: failed_json, status: 403
end
def destroy
sidebar_section = SidebarSection.find_by(id: section_params["id"])
@guardian.ensure_can_delete!(sidebar_section)

View File

@ -1607,7 +1607,6 @@ Discourse::Application.routes.draw do
delete "user-status" => "user_status#clear"
resources :sidebar_sections, only: %i[index create update destroy]
post "/sidebar_sections/reorder" => "sidebar_sections#reorder"
put "/sidebar_sections/reset/:id" => "sidebar_sections#reset"
get "*url", to: "permalinks#show", constraints: PermalinkConstraint.new

View File

@ -370,72 +370,6 @@ RSpec.describe SidebarSectionsController do
end
end
describe "#reorder" do
fab!(:user2) { Fabricate(:user) }
fab!(:sidebar_section) { Fabricate(:sidebar_section, user: user) }
fab!(:sidebar_url_1) { Fabricate(:sidebar_url, name: "tags", value: "/tags") }
fab!(:sidebar_url_2) { Fabricate(:sidebar_url, name: "categories", value: "/categories") }
fab!(:sidebar_url_3) { Fabricate(:sidebar_url, name: "topic", value: "/t/1") }
fab!(:section_link_1) do
Fabricate(:sidebar_section_link, sidebar_section: sidebar_section, linkable: sidebar_url_1)
end
fab!(:section_link_2) do
Fabricate(:sidebar_section_link, sidebar_section: sidebar_section, linkable: sidebar_url_2)
end
fab!(:section_link_3) do
Fabricate(:sidebar_section_link, sidebar_section: sidebar_section, linkable: sidebar_url_3)
end
it "sorts links" do
expect(sidebar_section.sidebar_urls.pluck(:id)).to eq(
[sidebar_url_1.id, sidebar_url_2.id, sidebar_url_3.id],
)
sign_in(user)
post "/sidebar_sections/reorder.json",
params: {
sidebar_section_id: sidebar_section.id,
links_order: [sidebar_url_2.id, sidebar_url_3.id, sidebar_url_1.id],
}
expect(response.status).to eq(200)
expect(sidebar_section.reload.sidebar_urls.pluck(:id)).to eq(
[sidebar_url_2.id, sidebar_url_3.id, sidebar_url_1.id],
)
end
it "returns 403 when a user tries to reorder a section that doesn't belong to them" do
sign_in(user2)
post "/sidebar_sections/reorder.json",
params: {
sidebar_section_id: sidebar_section.id,
links_order: [sidebar_url_2.id, sidebar_url_3.id, sidebar_url_1.id],
}
expect(response.status).to eq(403)
expect(sidebar_section.reload.sidebar_urls.pluck(:id)).to eq(
[sidebar_url_1.id, sidebar_url_2.id, sidebar_url_3.id],
)
end
it "returns 403 for an non user" do
post "/sidebar_sections/reorder.json",
params: {
sidebar_section_id: sidebar_section.id,
links_order: [sidebar_url_2.id, sidebar_url_3.id, sidebar_url_1.id],
}
expect(response.status).to eql(403)
end
end
describe "#destroy" do
fab!(:sidebar_section) { Fabricate(:sidebar_section, user: user) }

View File

@ -178,9 +178,12 @@ describe "Custom sidebar sections", type: :system do
["Sidebar Tags", "Sidebar Categories", "Sidebar Latest"],
)
tags_link = find(".sidebar-section-link[data-link-name='Sidebar Tags']")
latest_link = find(".sidebar-section-link[data-link-name='Sidebar Latest']")
sidebar.edit_custom_section("My section")
tags_link = find(".draggable[data-link-name='Sidebar Tags']")
latest_link = find(".draggable[data-link-name='Sidebar Latest']")
tags_link.drag_to(latest_link, html5: true, delay: 0.4)
section_modal.save
expect(sidebar.primary_section_links("my-section")).to eq(
["Sidebar Categories", "Sidebar Tags", "Sidebar Latest"],