data-table-utils.ts 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. import { AccessorFnColumnDef, ExpandedState } from '@tanstack/react-table';
  2. import { AccessorKeyColumnDef } from '@tanstack/table-core';
  3. /**
  4. * Returns the default column visibility configuration.
  5. *
  6. * @example
  7. * ```ts
  8. * const columnVisibility = getColumnVisibility(fields, {
  9. * id: false,
  10. * createdAt: false,
  11. * updatedAt: false,
  12. * });
  13. * ```
  14. */
  15. export function getColumnVisibility(
  16. columns: Array<AccessorKeyColumnDef<any> | AccessorFnColumnDef<any>>,
  17. defaultVisibility?: Record<string, boolean | undefined>,
  18. customFieldColumnNames?: string[],
  19. ): Record<string, boolean> {
  20. const allDefaultsTrue = defaultVisibility && Object.values(defaultVisibility).every(v => v === true);
  21. const allDefaultsFalse = defaultVisibility && Object.values(defaultVisibility).every(v => v === false);
  22. return {
  23. id: false,
  24. createdAt: false,
  25. updatedAt: false,
  26. ...(allDefaultsTrue ? { ...Object.fromEntries(columns.map(f => [f.id, false])) } : {}),
  27. ...(allDefaultsFalse ? { ...Object.fromEntries(columns.map(f => [f.id, true])) } : {}),
  28. // Make custom fields hidden by default unless overridden
  29. ...(customFieldColumnNames
  30. ? { ...Object.fromEntries(customFieldColumnNames.map(f => [f, false])) }
  31. : {}),
  32. ...defaultVisibility,
  33. selection: true,
  34. actions: true,
  35. };
  36. }
  37. /**
  38. * Ensures that the default column order always starts with `id`, `createdAt`, `deletedAt`
  39. */
  40. export function getStandardizedDefaultColumnOrder<T extends string | number | symbol>(
  41. defaultColumnOrder?: T[],
  42. ): T[] {
  43. const standardFirstColumns = new Set(['id', 'createdAt', 'updatedAt']);
  44. if (!defaultColumnOrder) {
  45. return [...standardFirstColumns] as T[];
  46. }
  47. const rest = defaultColumnOrder.filter(c => !standardFirstColumns.has(c as string));
  48. return [...standardFirstColumns, ...rest] as T[];
  49. }
  50. /**
  51. * Hierarchical item type with parent-child relationships
  52. */
  53. export interface HierarchicalItem {
  54. id: string;
  55. parentId?: string | null;
  56. breadcrumbs?: Array<{ id: string }>;
  57. children?: Array<{ id: string }> | null;
  58. }
  59. /**
  60. * Gets the parent ID of a hierarchical item
  61. */
  62. export function getItemParentId<T extends HierarchicalItem>(
  63. item: T | null | undefined,
  64. ): string | null | undefined {
  65. return item?.parentId || item?.breadcrumbs?.[0]?.id;
  66. }
  67. /**
  68. * Gets all siblings (items with the same parent) for a given parent ID
  69. */
  70. export function getItemSiblings<T extends HierarchicalItem>(
  71. items: T[],
  72. parentId: string | null | undefined,
  73. ): T[] {
  74. return items.filter(item => getItemParentId(item) === parentId);
  75. }
  76. /**
  77. * Checks if moving an item to a new parent would create a circular reference
  78. */
  79. export function isCircularReference<T extends HierarchicalItem>(
  80. item: T,
  81. targetParentId: string,
  82. items: T[],
  83. ): boolean {
  84. const targetParentItem = items.find(i => i.id === targetParentId);
  85. return (
  86. item.children?.some(child => {
  87. if (child.id === targetParentId) return true;
  88. const targetBreadcrumbIds = targetParentItem?.breadcrumbs?.map(b => b.id) || [];
  89. return targetBreadcrumbIds.includes(item.id);
  90. }) ?? false
  91. );
  92. }
  93. /**
  94. * Result of calculating the target position for a drag and drop operation
  95. */
  96. export interface TargetPosition {
  97. targetParentId: string;
  98. adjustedIndex: number;
  99. }
  100. /**
  101. * Context for drag and drop position calculation
  102. */
  103. interface DragContext<T extends HierarchicalItem> {
  104. item: T;
  105. targetItem: T | undefined;
  106. previousItem: T | null;
  107. isDraggingDown: boolean;
  108. isTargetExpanded: boolean;
  109. isPreviousExpanded: boolean;
  110. sourceParentId: string;
  111. items: T[];
  112. }
  113. /**
  114. * Checks if dragging down directly onto an expanded item
  115. */
  116. function isDroppingIntoExpandedTarget<T extends HierarchicalItem>(context: DragContext<T>): boolean {
  117. const { isDraggingDown, targetItem, item, isTargetExpanded } = context;
  118. return isDraggingDown && targetItem?.id !== item.id && isTargetExpanded;
  119. }
  120. /**
  121. * Checks if dragging down into an expanded item's children area
  122. */
  123. function isDroppingIntoExpandedPreviousChildren<T extends HierarchicalItem>(
  124. context: DragContext<T>,
  125. ): boolean {
  126. const { isDraggingDown, targetItem, previousItem, item, isPreviousExpanded } = context;
  127. return (
  128. isDraggingDown &&
  129. previousItem !== null &&
  130. targetItem?.id !== item.id &&
  131. isPreviousExpanded &&
  132. targetItem?.parentId === previousItem.id
  133. );
  134. }
  135. /**
  136. * Checks if dragging up into an expanded item's children area
  137. */
  138. function isDroppingIntoExpandedPreviousWhenDraggingUp<T extends HierarchicalItem>(
  139. context: DragContext<T>,
  140. ): boolean {
  141. const { isDraggingDown, previousItem, isPreviousExpanded } = context;
  142. return !isDraggingDown && previousItem !== null && isPreviousExpanded;
  143. }
  144. /**
  145. * Creates a position for dropping into an expanded item as first child
  146. */
  147. function createFirstChildPosition(parentId: string): TargetPosition {
  148. return { targetParentId: parentId, adjustedIndex: 0 };
  149. }
  150. /**
  151. * Calculates position for cross-parent drag operations
  152. */
  153. function calculateCrossParentPosition<T extends HierarchicalItem>(
  154. targetItem: T,
  155. sourceParentId: string,
  156. items: T[],
  157. ): TargetPosition | null {
  158. const targetItemParentId = getItemParentId(targetItem);
  159. if (!targetItemParentId || targetItemParentId === sourceParentId) {
  160. return null;
  161. }
  162. const targetSiblings = getItemSiblings(items, targetItemParentId);
  163. const adjustedIndex = targetSiblings.findIndex(i => i.id === targetItem.id);
  164. return { targetParentId: targetItemParentId, adjustedIndex };
  165. }
  166. /**
  167. * Calculates position when dropping at the end of the list
  168. */
  169. function calculateDropAtEndPosition<T extends HierarchicalItem>(
  170. previousItem: T | null,
  171. sourceParentId: string,
  172. items: T[],
  173. ): TargetPosition | null {
  174. if (!previousItem) {
  175. return null;
  176. }
  177. const previousItemParentId = getItemParentId(previousItem);
  178. if (!previousItemParentId || previousItemParentId === sourceParentId) {
  179. return null;
  180. }
  181. const targetSiblings = getItemSiblings(items, previousItemParentId);
  182. return { targetParentId: previousItemParentId, adjustedIndex: targetSiblings.length };
  183. }
  184. /**
  185. * Determines the target parent and index for a hierarchical drag and drop operation
  186. */
  187. export function calculateDragTargetPosition<T extends HierarchicalItem>(params: {
  188. item: T;
  189. oldIndex: number;
  190. newIndex: number;
  191. items: T[];
  192. sourceParentId: string;
  193. expanded: ExpandedState;
  194. }): TargetPosition {
  195. const { item, oldIndex, newIndex, items, sourceParentId, expanded } = params;
  196. const targetItem = items[newIndex];
  197. const previousItem = newIndex > 0 ? items[newIndex - 1] : null;
  198. const context: DragContext<T> = {
  199. item,
  200. targetItem,
  201. previousItem,
  202. isDraggingDown: oldIndex < newIndex,
  203. isTargetExpanded: targetItem ? !!expanded[targetItem.id as keyof ExpandedState] : false,
  204. isPreviousExpanded: previousItem ? !!expanded[previousItem.id as keyof ExpandedState] : false,
  205. sourceParentId,
  206. items,
  207. };
  208. // Handle dropping into expanded items (becomes first child)
  209. if (isDroppingIntoExpandedTarget(context)) {
  210. return createFirstChildPosition(targetItem.id);
  211. }
  212. if (previousItem && isDroppingIntoExpandedPreviousChildren(context)) {
  213. return createFirstChildPosition(previousItem.id);
  214. }
  215. if (previousItem && isDroppingIntoExpandedPreviousWhenDraggingUp(context)) {
  216. return createFirstChildPosition(previousItem.id);
  217. }
  218. // Handle cross-parent drag operations
  219. if (targetItem?.id !== item.id) {
  220. const crossParentPosition = calculateCrossParentPosition(targetItem, sourceParentId, items);
  221. if (crossParentPosition) {
  222. return crossParentPosition;
  223. }
  224. }
  225. // Handle dropping at the end of the list
  226. if (!targetItem && previousItem) {
  227. const dropAtEndPosition = calculateDropAtEndPosition(previousItem, sourceParentId, items);
  228. if (dropAtEndPosition) {
  229. return dropAtEndPosition;
  230. }
  231. }
  232. // Default: stay in the same parent at the beginning
  233. return { targetParentId: sourceParentId, adjustedIndex: 0 };
  234. }
  235. /**
  236. * Calculates the adjusted sibling index when reordering within the same parent
  237. */
  238. export function calculateSiblingIndex<T extends HierarchicalItem>(params: {
  239. item: T;
  240. oldIndex: number;
  241. newIndex: number;
  242. items: T[];
  243. parentId: string;
  244. }): number {
  245. const { item, oldIndex, newIndex, items, parentId } = params;
  246. const siblings = getItemSiblings(items, parentId);
  247. const oldSiblingIndex = siblings.findIndex(i => i.id === item.id);
  248. const isDraggingDown = oldIndex < newIndex;
  249. let newSiblingIndex = oldSiblingIndex;
  250. const [start, end] = isDraggingDown ? [oldIndex + 1, newIndex] : [newIndex, oldIndex - 1];
  251. for (let i = start; i <= end; i++) {
  252. if (getItemParentId(items[i]) === parentId) {
  253. newSiblingIndex += isDraggingDown ? 1 : -1;
  254. }
  255. }
  256. return newSiblingIndex;
  257. }