Skip to content

Commit

Permalink
Enable reordering of project resources (#668)
Browse files Browse the repository at this point in the history
* WIP prototype

* WIP Prototype

* WIP Prototype (edge detection)

* WIP index logic

* WIP index logic

* Save reorder on drop

* Cleanup

* Rearrange with custom preview

* Start of dropdown list

* WIP smart configuration

* Make drag setup dynamic

* Add moveUp/moveDown args to external links

* Small readOnly/itemsLength refactor

* Add icons to "move" menu

* Add dropdown menu functions

* Fix `toBottom` action; tweak menu item padding

* Improved drag affordances

* Improve hover / focus states

* Remove comment

* Remove unused getter

* Remove out-of-scope prettier changes

* Cleanup reorder function; add documentation

* Add documentation

* Remove `id` artifacts

* Close dropdown when dragging

* Announce movement onDrop

* CSS tweaks

* Refactor some CSS

* Add `TileMedium` test

* CSS cleanup

* Add `resource-test`

* Test icons

* Add drag-handle test

* Test rearranging docs

* Test reordering links

* Fix externalLink test

* Don't allow lists to drag/drop into each other

* Refactor `isHoveringSameParent`

* Focuses dragHandle after drop
  • Loading branch information
jeffdaley authored Apr 4, 2024
1 parent 913dece commit 69b4d2e
Show file tree
Hide file tree
Showing 21 changed files with 1,198 additions and 32 deletions.
4 changes: 3 additions & 1 deletion web/app/components/doc/tile-medium.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,10 @@
>
{{! Primary click area }}
<div
data-test-primary-click-area
aria-hidden="true"
class="absolute -top-px left-0 -bottom-px w-full mix-blend-multiply group-hover:bg-color-page-faint group-focus/primary-link:outline group-focus/primary-link:outline-2 group-focus/primary-link:outline-color-border-strong"
class="absolute -top-px left-0 -bottom-px w-full mix-blend-multiply group-focus-within:bg-color-page-faint group-hover:bg-color-page-faint group-focus/primary-link:outline group-focus/primary-link:outline-2 group-focus/primary-link:outline-color-border-strong
{{if @canBeReordered '-left-[20px] right-0 w-auto'}}"
></div>

<h3 class="relative">
Expand Down
8 changes: 7 additions & 1 deletion web/app/components/doc/tile-medium.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,20 @@ import { HermesDocument } from "hermes/types/document";
import { inject as service } from "@ember/service";
import FetchService from "hermes/services/fetch";
import ConfigService from "hermes/services/config";
import { DEFAULT_FILTERS } from "hermes/services/active-filters";

interface DocTileMediumComponentSignature {
Element: HTMLAnchorElement;
Args: {
doc: RelatedHermesDocument | HermesDocument;
avatarIsLoading?: boolean;

/**
* Whether the tile is part of a list that can be reordered.
* If true, we extend the hover/focus affordance into the gutter
* where the grab handle will be.
*/
canBeReordered?: boolean;

/**
* The search query associated with the current view.
* Used to highlight search terms in the document title.
Expand Down
20 changes: 18 additions & 2 deletions web/app/components/floating-u-i/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ interface FloatingUIContentSignature {
renderOut?: boolean;
offset?: OffsetOptions;
matchAnchorWidth?: MatchAnchorWidthOptions;
hide: () => void;
};
Blocks: {
default: [];
Expand Down Expand Up @@ -65,7 +66,7 @@ export default class FloatingUIContent extends Component<FloatingUIContentSignat
@action didInsert(e: HTMLElement) {
this._content = e;

const { matchAnchorWidth, anchor, placement } = this.args;
const { anchor, placement } = this.args;
const { content } = this;

this.maybeMatchAnchorWidth();
Expand All @@ -80,6 +81,18 @@ export default class FloatingUIContent extends Component<FloatingUIContentSignat
let updatePosition = async () => {
let _placement = placement || "bottom-start";

/**
* If anchor exists within a div that's being dragged, hide the content
* to prevent the dropdown from remaining open after its parent is dragged.
* The `is-dragging` class is added by Pragmatic Drag and Drop.
*/
const elementBeingDragged = document.querySelector(".is-dragging");

if (elementBeingDragged && elementBeingDragged.contains(anchor)) {
this.args.hide();
return;
}

computePosition(anchor, content, {
platform,
placement: _placement as Placement,
Expand All @@ -95,7 +108,10 @@ export default class FloatingUIContent extends Component<FloatingUIContentSignat
});
};

this.cleanup = autoUpdate(anchor, content, updatePosition);
this.cleanup = autoUpdate(anchor, content, updatePosition, {
// Recompute on layout shifts such as drag and drop.
layoutShift: true,
});
}

private maybeMatchAnchorWidth() {
Expand Down
1 change: 1 addition & 0 deletions web/app/components/floating-u-i/index.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
@anchor={{this.anchor}}
@id={{this.contentID}}
@matchAnchorWidth={{@matchAnchorWidth}}
@hide={{this.hideContent}}
...attributes
>
{{yield
Expand Down
28 changes: 22 additions & 6 deletions web/app/components/project/index.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,8 @@
@items={{this.hermesDocuments}}
@shouldAnimate={{this.shouldAnimate}}
@motion={{this.resourceListContainerMotion}}
@isReadOnly={{not this.projectIsActive}}
@onSave={{this.saveDocumentOrder}}
class="mt-7"
>
<:header>
Expand All @@ -170,7 +172,13 @@
<:item as |i|>
<RelatedResource::HermesDocument @resource={{i.item}} as |doc|>
<Project::Resource
@isReadOnly={{not this.projectIsActive}}
@isReadOnly={{i.isReadOnly}}
@canMoveUp={{i.canMoveUp}}
@canMoveDown={{i.canMoveDown}}
@moveToTop={{i.moveToTop}}
@moveUp={{i.moveUp}}
@moveDown={{i.moveDown}}
@moveToBottom={{i.moveToBottom}}
@overflowMenuItems={{hash
delete=(hash
label="Remove"
Expand All @@ -179,7 +187,7 @@
)
}}
>
<Doc::TileMedium @doc={{doc}} />
<Doc::TileMedium @canBeReordered={{true}} @doc={{doc}} />
</Project::Resource>
</RelatedResource::HermesDocument>
</:item>
Expand All @@ -196,6 +204,8 @@
@items={{this.externalLinks}}
@shouldAnimate={{this.shouldAnimate}}
@motion={{this.resourceListContainerMotion}}
@isReadOnly={{not this.projectIsActive}}
@onSave={{this.saveLinkOrder}}
>
<:header>
<span data-test-external-links-header>
Expand All @@ -204,7 +214,13 @@
</:header>
<:item as |i|>
<Project::Resource
@isReadOnly={{not this.projectIsActive}}
@isReadOnly={{i.isReadOnly}}
@canMoveUp={{i.canMoveUp}}
@canMoveDown={{i.canMoveDown}}
@moveToTop={{i.moveToTop}}
@moveUp={{i.moveUp}}
@moveDown={{i.moveDown}}
@moveToBottom={{i.moveToBottom}}
@overflowMenuItems={{hash
edit=(hash
label="Edit"
Expand All @@ -222,20 +238,20 @@
<ExternalLink
data-test-related-link
href={{link.url}}
class="block py-2.5 pl-2 pr-8 group-hover:bg-color-surface-faint"
class="block h-[34px] pt-[7px] pl-2 pr-8 group-hover:bg-color-surface-faint"
>
<div class="flex items-center gap-3">
<div class="ml-1 flex">
<Favicon @url={{link.url}} />
</div>
<div class="flex w-full min-w-0 items-end gap-2">
<h4
class="max-w-[65%] truncate text-display-300 font-semibold text-color-foreground-strong"
class="max-w-[60%] truncate text-body-200 font-semibold text-color-foreground-strong"
>
{{link.name}}
</h4>
<div
class="mb-px max-w-[35%] truncate text-body-200 text-color-foreground-faint"
class="max-w-[40%] truncate text-body-200 text-color-foreground-faint"
>
{{link.url}}
</div>
Expand Down
64 changes: 64 additions & 0 deletions web/app/components/project/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
RelatedExternalLink,
RelatedHermesDocument,
RelatedResource,
RelatedResourcesScope,
} from "../related-resources";
import { inject as service } from "@ember/service";
import FetchService from "hermes/services/fetch";
Expand Down Expand Up @@ -330,6 +331,69 @@ export default class ProjectIndexComponent extends Component<ProjectIndexCompone
this.editModalIsShown = true;
}

/**
* A triage method that saves the order of resources based on type.
* Caches the current array, removes the target resource from the array,
* inserts it at the new index, then patches the project.
*/
@action private saveResourcesOrder(
resourceType: RelatedResourcesScope,
currentIndex: number,
newIndex: number,
) {
const isDoc = resourceType === RelatedResourcesScope.Documents;

const cached = isDoc
? this.hermesDocuments.slice()
: this.externalLinks.slice();

const [removed] = isDoc
? this.hermesDocuments.splice(currentIndex, 1)
: this.externalLinks.splice(currentIndex, 1);

assert("removed must exist", removed);

if (resourceType === RelatedResourcesScope.Documents) {
assert("removed must be a document", "googleFileID" in removed);
this.hermesDocuments.insertAt(newIndex, removed);
void this.saveProjectResources.perform(
cached,
this.externalLinks.slice(),
);
} else {
assert("removed must be a link", "url" in removed);
this.externalLinks.insertAt(newIndex, removed);
void this.saveProjectResources.perform(
this.hermesDocuments.slice(),
cached,
);
}
}

/**
* The action to save the order of external links.
* Called when the user reorders a link in the list.
*/
@action protected saveLinkOrder(currentIndex: number, newIndex: number) {
this.saveResourcesOrder(
RelatedResourcesScope.ExternalLinks,
currentIndex,
newIndex,
);
}

/**
* The action to save the order of related documents.
* Called when the user reorders a document in the list.
*/
@action protected saveDocumentOrder(currentIndex: number, newIndex: number) {
this.saveResourcesOrder(
RelatedResourcesScope.Documents,
currentIndex,
newIndex,
);
}

/**
* The action to add a document to a project.
* Adds a resource to the correct array, then saves the project.
Expand Down
67 changes: 67 additions & 0 deletions web/app/components/project/resource-list-item.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<li
{{did-insert this.registerElement}}
data-index={{@index}}
data-closest-edge={{this.closestEdge}}
class="group relative block {{if this.isDragging 'is-dragging opacity-50'}}"
data-test-resource-list-item
...attributes
>
{{! Drop affordance }}
{{#if this.dragHasEntered}}
<div
class="absolute z-10 h-1 w-full rounded-l-full rounded-r-full
{{if this.closestEdge 'bg-color-foreground-action'}}
{{if
(eq this.closestEdge 'top')
'-top-px -translate-y-1/2'
(if (eq this.closestEdge 'bottom') 'bottom-0 translate-y-1/2')
}}"
>
</div>
{{/if}}

{{yield
(hash
moveToTop=this.moveToTop
moveUp=this.moveUp
moveDown=this.moveDown
moveToBottom=this.moveToBottom
)
}}

</li>

{{!
A hidden div rendered only if the list is interactive.
Used to configure the drag-and-drop behavior in a state-reactive way,
refreshing or initializing when the list changes from read only to interactive.
}}
{{#unless @isReadOnly}}
{{#in-element (html-element ".ember-application") insertBefore=null}}
<div
{{did-insert this.configureDragAndDrop}}
class="absolute top-0 left-0"
aria-hidden={{true}}
/>
{{/in-element}}
{{/unless}}

{{! Drag preview }}
{{#if this.isDragging}}
{{! Target the customNativeDragPreview container }}
{{! This element will be available by the time `html-element` runs }}
{{#in-element (html-element (concat "#" this.id)) insertBefore=null}}
<div
class="grid h-10 max-w-[500px] cursor-grabbing items-center rounded bg-color-page-primary px-3"
>
<div class="truncate font-semibold text-color-foreground-strong">
{{this.itemTitle}}
{{#if this.docNumber}}
<span class="ml-1 font-regular">
{{this.docNumber}}
</span>
{{/if}}
</div>
</div>
{{/in-element}}
{{/if}}
Loading

0 comments on commit 69b4d2e

Please sign in to comment.