LCOV - code coverage report
Current view: top level - dashql-app/src/session - session_list_rows.ts Coverage Total Hit
Test: DashQL 915828b Lines: 100.0 % 69 69
Test Date: 2026-11-06 19:30:32

            Line data    Source code
       1              : import {
       2              :     type SessionTreeFolder,
       3              :     folderAt,
       4              :     folderPathKey,
       5              :     isFolderPathWithin,
       6              :     sameFolderPath,
       7              : } from '../storage/session_tree.js';
       8              : 
       9              : /// The id of the drop target at the end of the session list, a drop there moves to the end of the root
      10            1 : export const TOP_LEVEL_DROP_ID = 'top-level';
      11              : 
      12              : /// A row of the session list, a folder or a session
      13              : export type SessionListRow<Item extends { sessionId: string }> =
      14              :     | {
      15              :         kind: 'folder';
      16              :         /// The sortable id
      17              :         id: string;
      18              :         folder: SessionTreeFolder;
      19              :         depth: number;
      20              :         expanded: boolean;
      21              :     }
      22              :     | {
      23              :         kind: 'session';
      24              :         /// The sortable id, the session id
      25              :         id: string;
      26              :         item: Item;
      27              :         depth: number;
      28              :         /// The folder of the session
      29              :         folderPath: string[];
      30              :     };
      31              : 
      32              : /// The rows of the session list: the subfolders of a folder before its sessions, the content of a
      33              : /// folder only when it is expanded. Sessions without an item, e.g. invalid ones, are left out.
      34            1 : export function flattenSessionListRows<Item extends { sessionId: string }>(
      35            1 :     tree: SessionTreeFolder,
      36            1 :     items: ReadonlyMap<string, Item>,
      37            1 :     isExpanded: (folder: SessionTreeFolder) => boolean,
      38            1 : ): SessionListRow<Item>[] {
      39            1 :     const rows: SessionListRow<Item>[] = [];
      40            1 :     const visit = (folder: SessionTreeFolder, depth: number) => {
      41            1 :         for (const child of folder.folders) {
      42            1 :             const expanded = isExpanded(child);
      43            1 :             rows.push({ kind: 'folder', id: `folder:${folderPathKey(child.path)}`, folder: child, depth, expanded });
      44            1 :             if (expanded) visit(child, depth + 1);
      45            1 :         }
      46            1 :         for (const entry of folder.sessions) {
      47            1 :             const item = items.get(entry.path);
      48            1 :             if (item != null) rows.push({ kind: 'session', id: entry.path, item, depth, folderPath: folder.path });
      49            1 :         }
      50            1 :     };
      51            1 :     visit(tree, 0);
      52            1 :     return rows;
      53            1 : }
      54              : 
      55              : /// Where a dragged row would land
      56              : export interface SessionListDrop {
      57              :     /// The folder that receives the row
      58              :     folderPath: string[];
      59              :     /// The position of a session among the sessions of the folder without it, past the end appends
      60              :     index: number;
      61              :     /// The row that shows the drop, or TOP_LEVEL_DROP_ID
      62              :     targetId: string;
      63              :     /// Does the row go into the target, or above or below it?
      64              :     position: 'into' | 'before' | 'after';
      65              : }
      66              : 
      67              : /// Resolve the drop of a dragged row on another row or on the drop target at the end of the list.
      68              : ///
      69              : /// A row dropped on a folder goes into it, a row dropped at the end of the list goes to the end of the root. A
      70              : /// session dropped on a session goes above or below it, in the folder of that session; a folder dropped on a
      71              : /// session goes into the folder of that session. Returns null when the drop changes nothing or is not possible,
      72              : /// e.g. a folder dropped into itself.
      73            1 : export function resolveSessionListDrop<Item extends { sessionId: string }>(
      74            1 :     rows: readonly SessionListRow<Item>[],
      75            1 :     tree: SessionTreeFolder,
      76            1 :     activeId: string,
      77            1 :     overId: string,
      78              :     /// Is the dragged row above the middle of the row it is dropped on?
      79            1 :     aboveMiddle: boolean,
      80            1 : ): SessionListDrop | null {
      81            1 :     if (activeId === overId) return null;
      82            1 :     const active = rows.find(row => row.id === activeId);
      83            1 :     if (active == null) return null;
      84            1 :     let drop: SessionListDrop;
      85            1 :     if (overId === TOP_LEVEL_DROP_ID) {
      86            1 :         drop = { folderPath: [], index: Number.MAX_SAFE_INTEGER, targetId: TOP_LEVEL_DROP_ID, position: 'into' };
      87            1 :     } else {
      88            1 :         const over = rows.find(row => row.id === overId);
      89            1 :         if (over == null) return null;
      90            1 :         if (over.kind === 'folder') {
      91            1 :             drop = { folderPath: over.folder.path, index: Number.MAX_SAFE_INTEGER, targetId: over.id, position: 'into' };
      92            1 :         } else if (active.kind === 'session') {
      93            1 :             const siblings = (folderAt(tree, over.folderPath)?.sessions ?? [])
      94            1 :                 .map(entry => entry.path)
      95            1 :                 .filter(id => id !== active.item.sessionId);
      96            1 :             const overIndex = siblings.indexOf(over.item.sessionId);
      97            1 :             drop = {
      98            1 :                 folderPath: over.folderPath,
      99            1 :                 index: aboveMiddle ? overIndex : overIndex + 1,
     100            1 :                 targetId: over.id,
     101            1 :                 position: aboveMiddle ? 'before' : 'after',
     102            1 :             };
     103            1 :         } else {
     104              :             // A folder goes into the folder of the session, shown on that folder or at the end of the list
     105            1 :             drop = over.folderPath.length === 0
     106            1 :                 ? { folderPath: [], index: Number.MAX_SAFE_INTEGER, targetId: TOP_LEVEL_DROP_ID, position: 'into' }
     107            1 :                 : { folderPath: over.folderPath, index: Number.MAX_SAFE_INTEGER, targetId: `folder:${folderPathKey(over.folderPath)}`, position: 'into' };
     108              :         }
     109            1 :     }
     110            1 :     if (active.kind === 'folder') {
     111            1 :         const path = active.folder.path;
     112            1 :         if (isFolderPathWithin(drop.folderPath, path) || sameFolderPath(drop.folderPath, path.slice(0, -1))) return null;
     113            1 :         return drop;
     114            1 :     }
     115              :     // A session that is already in place stays
     116            1 :     const current = folderAt(tree, active.folderPath)?.sessions.map(entry => entry.path) ?? [];
     117            1 :     const currentIndex = current.indexOf(active.item.sessionId);
     118            1 :     if (sameFolderPath(drop.folderPath, active.folderPath)) {
     119            1 :         const lastIndex = current.length - 1;
     120            1 :         const target = Math.min(drop.index, lastIndex);
     121            1 :         if (target === currentIndex) return null;
     122            1 :     }
     123            1 :     return drop;
     124            1 : }
        

Generated by: LCOV version 2.0-1