diff --git a/src/renderer/components/team/kanban/KanbanGridLayout.tsx b/src/renderer/components/team/kanban/KanbanGridLayout.tsx index 329c1e64..4e4d84f3 100644 --- a/src/renderer/components/team/kanban/KanbanGridLayout.tsx +++ b/src/renderer/components/team/kanban/KanbanGridLayout.tsx @@ -1,5 +1,5 @@ /* eslint-disable tailwindcss/no-custom-classname -- this adapter needs stable non-Tailwind class hooks for react-grid-layout handles. */ -import { useCallback, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import ReactGridLayout, { WidthProvider } from 'react-grid-layout/legacy'; import { usePersistedGridLayout } from '@renderer/hooks/usePersistedGridLayout'; @@ -25,6 +25,7 @@ const DEFAULT_ITEM_HEIGHT = Math.max( const DEFAULT_MIN_HEIGHT = 10; const DEFAULT_MIN_WIDTH = 3; const GRID_SCOPE_KEY = 'kanban-grid-layout:global'; +const SKELETON_HIDE_DELAY_MS = 500; const RESIZE_HANDLES: ResizeHandleAxis[] = ['s', 'w', 'e', 'n', 'sw', 'nw', 'se', 'ne']; const WidthAwareGridLayout = WidthProvider(ReactGridLayout); @@ -49,6 +50,11 @@ interface LoadedKanbanGridLayoutProps { readonly onPersistLayout: (layout: Layout, options?: { persist?: boolean }) => void; } +interface LoadingKanbanGridLayoutProps { + readonly columns: KanbanGridColumn[]; + readonly visibleItems: PersistedGridLayoutItem[]; +} + function buildDefaultItems(itemIds: string[]): PersistedGridLayoutItem[] { return itemIds.map((id, index) => ({ id, @@ -99,6 +105,58 @@ function renderResizeHandle(axis: ResizeHandleAxis, ref: Ref): Reac ); } +const LoadingKanbanGridLayout = ({ + columns, + visibleItems, +}: Readonly): ReactElement => { + const columnMap = new Map(columns.map((column) => [column.id, column])); + const loadingItems = + visibleItems.length > 0 + ? visibleItems + : buildDefaultItems(columns.length > 0 ? columns.map((column) => column.id) : ['todo']); + + return ( +
+
+ {loadingItems.map((item) => { + const column = columnMap.get(item.id as KanbanColumnId); + + return ( +
+
+
+
+
+
+
+
+
+
+
+
+ ); + })} +
+
+ ); +}; + const LoadedKanbanGridLayout = ({ columns, visibleItems, @@ -188,6 +246,19 @@ export const KanbanGridLayout = ({ repository: browserGridLayoutRepository, buildDefaultItems, }); + const [showResolvedLayout, setShowResolvedLayout] = useState(false); + + useEffect(() => { + if (!isLoaded || showResolvedLayout) return; + + const timeoutId = window.setTimeout(() => { + setShowResolvedLayout(true); + }, SKELETON_HIDE_DELAY_MS); + + return () => { + window.clearTimeout(timeoutId); + }; + }, [isLoaded, showResolvedLayout]); const applyReactGridLayout = useCallback( (layout: Layout, options?: { persist?: boolean }) => { @@ -198,13 +269,11 @@ export const KanbanGridLayout = ({ [applyVisibleItems] ); - if (!isLoaded) { - return
; + if (!isLoaded || !showResolvedLayout) { + return ; } - const gridKey = visibleItems - .map((item) => `${item.id}:${item.x}:${item.y}:${item.w}:${item.h}`) - .join('|'); + const gridKey = visibleItems.map((item) => item.id).join('|'); return ( buildDefaultItems(allItemIds), [allItemIds, buildDefaultItems] ); - const [layoutState, setLayoutState] = useState(() => - normalizePersistedGridLayoutState(null, defaultItems) + const initialState = useMemo( + () => normalizePersistedGridLayoutState(repository.peek?.(scopeKey) ?? null, defaultItems), + [defaultItems, repository, scopeKey] ); + const [layoutState, setLayoutState] = useState(() => initialState); const [loadedScopeKey, setLoadedScopeKey] = useState(null); const resolvedLayoutState = useMemo( () => normalizePersistedGridLayoutState(layoutState, defaultItems), diff --git a/src/renderer/index.css b/src/renderer/index.css index 74fa0dba..afb32910 100644 --- a/src/renderer/index.css +++ b/src/renderer/index.css @@ -263,10 +263,11 @@ .kanban-grid-layout { min-height: 640px; overflow: visible; + transition: none; } .kanban-grid-layout .react-grid-item { - transition-property: transform, width, height; + transition: none; overflow: visible; } diff --git a/src/renderer/services/layout-system/BrowserGridLayoutRepository.ts b/src/renderer/services/layout-system/BrowserGridLayoutRepository.ts index 629f13a1..2ad360a7 100644 --- a/src/renderer/services/layout-system/BrowserGridLayoutRepository.ts +++ b/src/renderer/services/layout-system/BrowserGridLayoutRepository.ts @@ -51,9 +51,14 @@ export class BrowserGridLayoutRepository implements GridLayoutRepository(); + peek(scopeKey: string): PersistedGridLayoutState | null { + const key = storageKey(scopeKey); + return pickNewestState(this.fallbackStore.get(key) ?? null, readLocalStorage(key)); + } + async load(scopeKey: string): Promise { const key = storageKey(scopeKey); - const memoryState = this.fallbackStore.get(key) ?? null; + const memoryState = this.peek(scopeKey); const localState = readLocalStorage(key); let idbState: PersistedGridLayoutState | null = null; diff --git a/src/renderer/services/layout-system/GridLayoutRepository.ts b/src/renderer/services/layout-system/GridLayoutRepository.ts index 8d71370a..33e62de5 100644 --- a/src/renderer/services/layout-system/GridLayoutRepository.ts +++ b/src/renderer/services/layout-system/GridLayoutRepository.ts @@ -1,6 +1,7 @@ import type { PersistedGridLayoutState } from './gridLayoutTypes'; export interface GridLayoutRepository { + peek?(scopeKey: string): TState | null; load(scopeKey: string): Promise; save(scopeKey: string, state: TState): Promise; clear(scopeKey: string): Promise;