5

I'm trying to implement a kanban agile board like Trello in my application. I'm wondering how to do it the best way. I'm considering these entities:

Board
has many Lists

List
has many Cards

Card
contains some content

However I'm stuck with ordering of Cards. Each Card should have a sort position to sort Cards in particular order in a List. When dragging a Card, I should change it's position and save it in the database. What is the most efficient way of doing this?

Adding a position field to each Card seems redundant since I would have to recalculate positions of all Cards in a List (or two) after I drag a Card to a different position (there will be hundreds of Cards). I was thinking of storing an array of all Card ids in a List and sort Cards by this array. What are pros/cons of this solution? And are there any better solutions?

I'm using Ruby on Rails and PostgreSQL.

UPDATE

Using @cske answer I came up with the following solution:

CREATE OR REPLACE FUNCTION move_buyer_card(
    new_list_id   INT
  , param_id      INT
  , new_position  INT
) RETURNS FLOAT4
LANGUAGE plpgsql SECURITY DEFINER
AS $$
DECLARE
  var_lower_bound FLOAT4;
  var_upper_bound FLOAT4;
  var_new_weight  FLOAT4; /*between 0 and 1*/
BEGIN
  IF new_position < 2 THEN /*first position*/
    var_lower_bound := 0;
    SELECT MIN(weight) FROM Buyers
      WHERE board_list_id = new_list_id
      INTO var_upper_bound;
    IF var_upper_bound IS NULL THEN /*empty list*/
      var_upper_bound := 1;
    END IF;
  ELSE /*not first position*/
    WITH ordered_cards AS (
      SELECT id, RANK() OVER (ORDER BY weight ASC) AS rank, weight
      FROM Buyers WHERE board_list_id = new_list_id
    )
    SELECT cards0.weight, cards1.weight from ordered_cards cards0
      JOIN ordered_cards cards1
        ON cards0.rank = cards1.rank - 1
      WHERE cards1.rank = new_position
      INTO var_lower_bound, var_upper_bound;
    IF NOT FOUND THEN /*only 1 item in list OR last position*/
      SELECT MAX(weight) FROM Buyers WHERE board_list_id = new_list_id
        INTO var_lower_bound;
      IF var_lower_bound IS NULL THEN /*empty list*/
        var_lower_bound := 0;
      END IF;
      var_upper_bound := 1;
    END IF;
  END IF;
  var_new_weight := var_lower_bound + (var_upper_bound - var_lower_bound) / 2;
  UPDATE Buyers
    SET weight = var_new_weight,
      board_list_id = new_list_id
    WHERE id = param_id;
  RETURN var_new_weight;
END;
$$;
B--rian
  • 5,578
  • 10
  • 38
  • 89
leemour
  • 11,414
  • 7
  • 36
  • 43

1 Answers1

2

Consider this, trick is not storing the position, but a weight so you can insert between any two elements

create table listOfCards (
   listId INTEGER
  ,cardId INTEGER
  ,weigth FLOAT4
  ,PRIMARY KEY (listId,cardId)
);

CREATE OR REPLACE FUNCTION addCard(
    plistId INT
  , pcardId INT
  , ppos    INT
) RETURNS FLOAT4
LANGUAGE plpgsql SECURITY DEFINER
AS $$
DECLARE
  vlb FLOAT4;
  vub FLOAT4;
  vnw FLOAT4;
BEGIN
  IF 2 > ppos THEN
    vlb := 0;
    SELECT min(weigth) FROM listOfCards WHERE listId = plistId INTO vub;
    IF vub IS NULL THEN /*empty list*/
      vub := 2;
    END IF; 
  ELSE
    with corder as (select cardId,RANK() OVER (order by weigth asc) as r,weigth FROM listOfCards WHERE listId=1)
    select c0.weigth,c1.weigth from corder c0 JOIN corder c1 ON c1.r = c0.r + 1 where c1.r = ppos INTO vlb,vub;
    IF NOT FOUND THEN
      SELECT max(weigth) FROM listOfCards WHERE listId = plistId INTO vlb;
      IF vlb IS NULL THEN /*empty list*/
        vlb := 0;
      END IF;
      vub := (vlb+1) * 2;
    END IF;  
  END IF;
  vnw := vlb + (vub-vlb) /2;
  INSERT INTO listOfCards(listId, cardId, weigth) 
       VALUES (plistId,pcardId,vnw )
  ON CONFLICT ON CONSTRAINT listofcards_pkey DO UPDATE SET weigth = vnw ;
  RETURN vnw;
END;
$$;

Usage:

select addCard(1,1,1);
select addCard(1,2,1);
select addCard(1,3,2);
select addCard(1,4,2);
select addCard(1,5,5);
select addCard(1,5,2);

select * from listOfCards ORDER BY weigth;

Result: 1,2,0.5 1,5,0.5625 1,4,0.625 1,3,0.75 1,1,1

cske
  • 2,233
  • 4
  • 26
  • 24
  • Thanks for your answer. It's a working solution. I updated my question with a slightly modified code - could you comment on that? I think my solution is slightly simpler (less confusing numbers). – leemour Dec 12 '16 at 13:20
  • @leemour if that is simpler just use it (your code requires param_id to be already in Buyers (update at the end) but not checks it), don't forget properly test edge cases (empty list, move in single element list, ...), table structure is missing from question it is hard to tell more – cske Dec 12 '16 at 15:22
  • Yes, I fully tested my version against all edge cases. Your solution sets last item's weight to 1, and adding an item after that would set its weight to a value > 1. Would you mind updating your solution to account for that so I can accept it? I replaced `vub := 2;` with `vub := 1;`, `ON c1.r = c0.r + 1` with `ON c0.r = c1.r - 1` and `vub := (vlb+1) * 2;` with `vub := 1` – leemour Dec 12 '16 at 21:53
  • @cske Hi guys, this topic's very great to me. I just wonder if we have 10000 cards => is that okay if we still use weight to order cards? I'm worry about data type "float"? – DFX Nguyễn Dec 28 '21 at 07:29