scrumlr.io icon indicating copy to clipboard operation
scrumlr.io copied to clipboard

Reorder notes within board view

Open timengel opened this issue 2 years ago • 1 comments

Description

As a moderator I want to reorder and combine notes within the board view by drag & drop.

Changelog

  • Replaced react-dnd with dnd-kit which supports grid layout sorting and combining in parallel.

Checklist

  • [ ] I have performed a self-review of my own code
  • [ ] I have commented my code, particularly in hard-to-understand areas
  • [ ] The light- and dark-theme are both supported and tested
  • [ ] The design was implemented and is responsive for all devices and screen sizes
  • [ ] The application was tested in the most commonly used browsers (e.g. Chrome, Firefox, Safari)

timengel avatar Jun 05 '22 15:06 timengel

This should be the code from this example:

import {
    Active, Announcements, Collision, CollisionDetection, defaultDropAnimation, DndContext, DragOverlay, DropAnimation, KeyboardSensor, MeasuringConfiguration, Modifiers,
    MouseSensor, PointerActivationConstraint, rectIntersection, ScreenReaderInstructions,
    TouchSensor,
    UniqueIdentifier,
    useSensor,
    useSensors
} from '@dnd-kit/core';
import {
    AnimateLayoutChanges, arrayMove, NewIndexGetter, rectSortingStrategy, SortableContext,
    sortableKeyboardCoordinates,
    SortingStrategy, useSortable
} from '@dnd-kit/sortable';
import React, { useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { Item, List, Wrapper } from '../../components';
import { createRange } from '../../utilities';



export interface Props {
  activationConstraint?: PointerActivationConstraint;
  animateLayoutChanges?: AnimateLayoutChanges;
  adjustScale?: boolean;
  collisionDetection?: CollisionDetection;
  Container?: any; // To-do: Fix me
  dropAnimation?: DropAnimation | null;
  getNewIndex?: NewIndexGetter;
  handle?: boolean;
  itemCount?: number;
  items?: string[];
  measuring?: MeasuringConfiguration;
  modifiers?: Modifiers;
  renderItem?: any;
  removable?: boolean;
  reorderItems?: typeof arrayMove;
  strategy?: SortingStrategy;
  style?: React.CSSProperties;
  useDragOverlay?: boolean;

  getItemStyles?(args: {
      id: UniqueIdentifier;
      index: number;
      isSorting: boolean;
      isDragOverlay: boolean;
      overIndex: number;
      isDragging: boolean;
      shouldCombine?: boolean;
    }): React.CSSProperties;

  wrapperStyle?(args: {
      index: number;
      isDragging: boolean;
      id: string;
    }): React.CSSProperties;

  isDisabled?(id: UniqueIdentifier): boolean;
}

const defaultDropAnimationConfig: DropAnimation = {
  ...defaultDropAnimation,
  dragSourceOpacity: 0.5,
};

const screenReaderInstructions: ScreenReaderInstructions = {
  draggable: `
  To pick up a sortable item, press the space bar.
  While sorting, use the arrow keys to move the item.
  Press space again to drop the item in its new position, or press escape to cancel.
`,
};

export function SortableWithCombine({
  activationConstraint,
  animateLayoutChanges,
  adjustScale = false,
  Container = List,
  collisionDetection = rectIntersection,
  dropAnimation = defaultDropAnimationConfig,
  getItemStyles = () => ({}),
  getNewIndex,
  handle = false,
  itemCount = 16,
  items: initialItems,
  isDisabled = () => false,
  measuring,
  modifiers,
  removable,
  renderItem,
  reorderItems = arrayMove,
  strategy = rectSortingStrategy,
  style,
  useDragOverlay = true,
  wrapperStyle = () => ({}),
}: Props) {

  const [items, setItems] = useState<string[]>(
      () =>
      initialItems ??
      createRange<string>(itemCount, (index) => (index  1).toString())
  );

  const [activeId, setActiveId] = useState<string | null>(null);
  const sensors = useSensors(
      useSensor(MouseSensor, {
          activationConstraint,
        }),
    useSensor(TouchSensor, {
        activationConstraint,
      }),
    useSensor(KeyboardSensor, {
        coordinateGetter: sortableKeyboardCoordinates,
      })
  );

  const isFirstAnnouncement = useRef(true);
  const getIndex = items.indexOf.bind(items);
  const getPosition = (id: string) => getIndex(id)  1;
  const activeIndex = activeId ? getIndex(activeId) : -1;
  const handleRemove = removable
    ? (id: string) => setItems((items) => items.filter((item) => item !== id))
    : undefined;
    
  const announcements: Announcements = {
    onDragStart(id) {
      return `Picked up sortable item ${id}. Sortable item ${id} is in position ${getPosition(
          id
        )} of ${items.length}`;
    },sortablewithcom
    onDragOver(id, overId) {
        // In this specific use-case, the picked up item's `id` is always the same as the first `over` id.
          // The first `onDragOver` event therefore doesn't need to be announced, because it is called
            // immediately after the `onDragStart` announcement and is redundant.
              if (isFirstAnnouncement.current === true) {
            isFirstAnnouncement.current = false;
            return;
          }

          if (overId) {
            return `Sortable item ${id} was moved into position ${getPosition(
                overId
              )} of ${items.length}`;
          }

          return;
      },
    onDragEnd(id, overId) {
        if (overId) {
            return `Sortable item ${id} was dropped at position ${getPosition(
                overId
              )} of ${items.length}`;
          }

          return;
      },
    onDragCancel(id) {
        return `Sorting was cancelled. Sortable item ${id} was dropped and returned to position ${getPosition(
            id
          )} of ${items.length}.`;
      },
    };

    useEffect(() => {
        if (!activeId) {
            isFirstAnnouncement.current = true;
          }
      }, [activeId]);

      return (
        <DndContext
      announcements={announcements}
        screenReaderInstructions={screenReaderInstructions}
        sensors={sensors}
        collisionDetection={collisionDetection}
        onDragStart={({active}) => {
            if (!active) {
                return;
              }

              setActiveId(active.id);
          }}
        onDragEnd={({over, collisions, active}) => {
            const [secondCollidingId, collisionRatio] =
                collisions && collisions.length > 1 ? collisions[1] : [];
            if (secondCollidingId) {
                const shouldCombine =
                    secondCollidingId !== active.id &&
                    collisionRatio != null &&
                    collisionRatio > 0.1 &&
                    collisionRatio < 0.4;
                if (shouldCombine && activeId) {
                    const newItems = Object.assign([], items);
                    console.log('COMBINING: ', activeId, ' with ', secondCollidingId);
                    newItems.splice(getIndex(activeId), 1);
                    setItems(newItems);
                    return;
                  }
              }

              if (over) {
                const overIndex = getIndex(over.id);
                if (activeIndex !== overIndex) {
                    setItems((items) => reorderItems(items, activeIndex, overIndex));
                  }
              }
            setActiveId(null);
          }}
        onDragCancel={() => setActiveId(null)}
        measuring={measuring}
        modifiers={modifiers}
      >
        <Wrapper style={style} center>
            <SortableContext items={items} strategy={strategy}>
              <Container>
                {items.map((value, index) => (
                  <SortableItem
                    key={value}
                    id={value}
                    handle={handle}
                    index={index}
                    style={getItemStyles}
                    wrapperStyle={wrapperStyle}
                    disabled={isDisabled(value)}
                    renderItem={renderItem}
                    onRemove={handleRemove}
                    animateLayoutChanges={animateLayoutChanges}
                    useDragOverlay={useDragOverlay}
                    getNewIndex={getNewIndex}
                  />
                ))}
              </Container>
            </SortableContext>
          </Wrapper>
        {useDragOverlay
          ? createPortal(
                <DragOverlay
              adjustScale={adjustScale}
                dropAnimation={dropAnimation}
                >
                  {activeId ? (
                        <Item
                  value={items[activeIndex]}
                    handle={handle}
                    renderItem={renderItem}
                    wrapperStyle={wrapperStyle({
                        index: activeIndex,
                        isDragging: true,
                        id: items[activeIndex],
                      })}
                    style={getItemStyles({
                        id: items[activeIndex],
                        index: activeIndex,
                        isSorting: activeId !== null,
                        isDragging: true,
                        overIndex: -1,
                        isDragOverlay: true,
                      })}
                    dragOverlay
                  />
                ) : null}
              </DragOverlay>,
              document.body
            )
          : null}
      </DndContext>
    );
  }

  interface SortableItemProps {
    animateLayoutChanges?: AnimateLayoutChanges;
    disabled?: boolean;
    getNewIndex?: NewIndexGetter;
    id: string;
    index: number;
    handle: boolean;
    useDragOverlay?: boolean;
    onRemove?(id: string): void;
    style(values: any): React.CSSProperties;
    renderItem?(args: any): React.ReactElement;
    wrapperStyle({
        index,
        isDragging,
        id,
      }: {
        index: number;
        isDragging: boolean;
        id: string;
      }): React.CSSProperties;
  }

  export function SortableItem({
  disabled,
    animateLayoutChanges,
    getNewIndex,
    handle,
    id,
    index,
    onRemove,
    style,
    renderItem,
    useDragOverlay,
    wrapperStyle,
  }: SortableItemProps) {

    const {
      active,
        attributes,
        collisions,
        isDragging,
        isSorting,
        items,
        listeners,
        newIndex,
        overIndex,
        setNodeRef,
        transform,
        transition,
      } = useSortable({
        id,
        animateLayoutChanges,
        disabled,
        getNewIndex,
      });

      const shouldCombine = getShouldCombine(
        id,
        items,
        newIndex,
        collisions,
        active
      );

      return (
        <Item
      ref={setNodeRef}
        value={id}
        disabled={disabled}
        dragging={isDragging}
        sorting={isSorting}
        handle={handle}
        renderItem={renderItem}
        index={index}
        style={style({
            index,
            id,
            isDragging,
            isSorting,
            overIndex,
            shouldCombine,
          })}
        onRemove={onRemove ? () => onRemove(id) : undefined}
        transform={transform}
        transition={!useDragOverlay && isDragging ? 'none' : transition}
        wrapperStyle={wrapperStyle({index, isDragging, id})}
        listeners={listeners}
        data-index={index}
        data-id={id}
        dragOverlay={!useDragOverlay && isDragging}
        {...attributes}
      />
    );
  }

  const getShouldCombine = (
    id: string,
    items: string[],
    newIndex: number,
    collisions: Collision[] | null,
    active: Active | null
) => {
    if (!items) {
        return false;
      }
    const newId = items[newIndex];

      const [secondCollidingId, collisionRatio] =
        collisions && collisions.length > 1 ? collisions[1] : [];

      return (
        secondCollidingId === newId &&
        collisionRatio != null &&
        id !== active?.id &&
        collisionRatio > 0.1 &&
        collisionRatio < 0.4
      );
  };

timengel avatar Jun 14 '22 08:06 timengel

Closed due to the code not being up to date anymore, but this pull request can be looked up to on future assignments.

brandstetterm avatar Dec 01 '22 13:12 brandstetterm