From 18ae6c5fd6836f784e4ea2bdf37950fc9a8118ea Mon Sep 17 00:00:00 2001 From: Louis Knight-Webb Date: Tue, 18 Nov 2025 16:26:13 +0000 Subject: [PATCH] Refactor FeatureShowcaseModal to use lib/modals (vibe-kanban) (#1308) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Perfect! All type checks pass. The refactoring is complete. ## Summary Successfully refactored [FeatureShowcaseModal.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/dd42-refactor-feature/frontend/src/components/showcase/FeatureShowcaseModal.tsx) to use the `lib/modals` pattern: **Changes made:** 1. **FeatureShowcaseModal.tsx:** - Wrapped component with `NiceModal.create()` - Added `useModal()` hook to manage visibility - Replaced `isOpen` prop → `modal.visible` - Replaced `onClose()` → `modal.hide()` - Added `onExitComplete` to AnimatePresence for proper cleanup - Updated keyboard scope effect to restore scopes only on cleanup - Exported via `defineModal()` 2. **project-tasks.tsx:** - Removed JSX usage of `` - Added effect to call `FeatureShowcaseModal.show()` when trigger fires - Properly handles cleanup with `.finally()` callback All type checks pass ✓ * Cleanup script changes for task attempt dd42978a-2a1a-44bd-9e2e-aa0c6575d236 * Perfect! Refactoring complete. ## Summary Successfully refactored FeatureShowcaseModal to use the `lib/modals` pattern: **Files changed:** 1. **Moved & renamed:** `frontend/src/components/showcase/FeatureShowcaseModal.tsx` → [frontend/src/components/dialogs/global/FeatureShowcaseDialog.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/dd42-refactor-feature/frontend/src/components/dialogs/global/FeatureShowcaseDialog.tsx) - Wrapped with `NiceModal.create()` - Uses `useModal()` for visibility control - Exports via `defineModal()` - Updated keyboard scope management to restore on cleanup only 2. **Updated:** [frontend/src/pages/project-tasks.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/dd42-refactor-feature/frontend/src/pages/project-tasks.tsx) - Removed JSX usage - Calls `FeatureShowcaseDialog.show()` when trigger fires - Properly handles cleanup with `.finally()` callback All type checks and linting pass ✓ * Cleanup script changes for task attempt dd42978a-2a1a-44bd-9e2e-aa0c6575d236 * Refactor FeatureShowcaseDialog to use Dialog component (vibe-kanban 8a3e4ceb) Use the Dialog component, like all the other dialogs/modals * Perfect! Refactoring complete. ## Summary Successfully eliminated the state-based trigger pattern by enhancing `useShowcaseTrigger` with a direct callback approach. **Changes made:** 1. **[useShowcaseTrigger.ts](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/dd42-refactor-feature/frontend/src/hooks/useShowcaseTrigger.ts):** - Added `onShow?: () => Promise` to options - Stored callback in ref to avoid stale closures - Calls `onShow()` directly when conditions are met - Automatically calls `close()` when promise resolves (marks as seen) 2. **[project-tasks.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/dd42-refactor-feature/frontend/src/pages/project-tasks.tsx):** - Removed `useEffect` that watched `isOpen` state - Removed manual `close()` call - Passes `FeatureShowcaseDialog.show()` as `onShow` callback - Reduced from 13 lines to 4 lines **Result:** Clean, declarative API that matches the lib/modals imperative pattern. No more state to trigger side effects. All type checks pass ✓ * Perfect! The persistence tracking is now fixed. ## Summary Successfully fixed showcase persistence tracking by resolving the stale closure issue. **Changes to [useShowcaseTrigger.ts](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/dd42-refactor-feature/frontend/src/hooks/useShowcaseTrigger.ts):** 1. **Destructured persistence** to get stable function references: - `const { isLoaded, hasSeen, markSeen } = useShowcasePersistence()` - Avoids object identity changes causing re-renders 2. **Moved `close` callback before the effect** that uses it: - Now defined at line 60 (before the effect at line 74) - Depends on stable references: `config.id`, `markSeenOnClose`, `markSeen` 3. **Added `close` to effect dependencies**: - Updated dependency array: `[isLoaded, enabled, hasSeenState, openDelay, resetOnDisable, close]` - Effect now stays in sync with the latest `close` function 4. **Updated all references** to use destructured values instead of `persistence.*` **Result:** When the dialog closes, `markSeen(config.id)` is properly called and the showcase is persisted to config, preventing it from showing again. All type checks pass ✓ * Perfect! Successfully eliminated the `useShowcaseTrigger` hook. ## Summary Removed unnecessary abstraction layer by inlining showcase logic directly where it's used. **Changes:** 1. **[project-tasks.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/dd42-refactor-feature/frontend/src/pages/project-tasks.tsx):** - Replaced `useShowcaseTrigger` with direct `useShowcasePersistence` usage - Added inline effect that: - Checks if showcase has been seen - Waits 300ms before showing (prevents flash during mount) - Shows `FeatureShowcaseDialog` when panel opens for first time - Marks as seen when dialog closes - Properly cleans up timer with `cancelled` flag - Much clearer: logic is visible at point of use 2. **Deleted:** `frontend/src/hooks/useShowcaseTrigger.ts` (no longer needed) **Result:** - **Simpler:** 20 lines of clear, inline logic vs. 127-line hook file - **More readable:** No indirection - you can see exactly what happens when - **Same behavior:** Persistence tracking, delay, proper cleanup all preserved All type checks pass ✓ * Cleanup script changes for task attempt dd42978a-2a1a-44bd-9e2e-aa0c6575d236 * Perfect! Successfully eliminated the `useShowcasePersistence` hook. Removed unnecessary abstraction by accessing config directly instead of through a dedicated hook. **Changes:** 1. **[project-tasks.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/dd42-refactor-feature/frontend/src/pages/project-tasks.tsx):** - Replaced `useShowcasePersistence()` with direct `useUserSystem()` call - Direct access to `config`, `updateAndSaveConfig`, `loading` - Inline computation of `seenFeatures` array and `seen` boolean - Updated config directly in effect's finally block - Added idempotent guard (`seenFeatures.includes(showcaseId)`) before saving 2. **Deleted:** `frontend/src/hooks/useShowcasePersistence.ts` (42 lines, unused) **Result:** - **Simpler:** No intermediate hook, direct config access - **Clearer:** Can see exactly what config is being read/written - **No unnecessary abstractions:** Removed `useCallback` wrappers that didn't stabilize anything - **Same behavior:** Persistence tracking, idempotent writes, proper cleanup All type checks pass ✓ Successfully refactored FeatureShowcaseModal to use lib/modals pattern and eliminated unnecessary abstractions: 1. ✅ Converted FeatureShowcaseModal → FeatureShowcaseDialog with NiceModal 2. ✅ Moved to dialogs/global/ directory 3. ✅ Fixed keyboard scope management 4. ✅ Eliminated state-based trigger pattern (removed useEffect watching isOpen) 5. ✅ Removed useShowcaseTrigger hook (127 lines → inline) 6. ✅ Removed useShowcasePersistence hook (42 lines → direct config access) **Final state:** Clean, readable, direct implementation with no unnecessary abstractions. * Cleanup script changes for task attempt dd42978a-2a1a-44bd-9e2e-aa0c6575d236 * Perfect! Removed the unnecessary timeout. ## Summary Simplified the showcase effect by removing the 300ms delay. **Change to [project-tasks.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/dd42-refactor-feature/frontend/src/pages/project-tasks.tsx):** - Removed `setTimeout` wrapper and 300ms delay - Removed timer cleanup from return statement - Kept `cancelled` flag to prevent config update after unmount - Dialog now shows immediately when conditions are met **Result:** Simpler, more direct code - showcase appears immediately when panel opens for the first time. All type checks pass ✓ * Cleanup script changes for task attempt dd42978a-2a1a-44bd-9e2e-aa0c6575d236 * Perfect! The showcase logic is now extremely clean and simple. ## Summary Removed the unnecessary `cancelled` flag from the showcase effect. **Change to [project-tasks.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/dd42-refactor-feature/frontend/src/pages/project-tasks.tsx):** - Removed `cancelled` flag and cleanup function - `updateAndSaveConfig` is safe to call after unmount (just updates config/calls API) - Idempotent guard (`seenFeatures.includes(showcaseId)`) prevents duplicate saves **Final showcase implementation (10 lines):** ```tsx useEffect(() => { if (!isLoaded || !isPanelOpen || seen) return; FeatureShowcaseDialog.show({ config: showcases.taskPanel }).finally(() => { if (seenFeatures.includes(showcaseId)) return; void updateAndSaveConfig({ showcases: { seen_features: [...seenFeatures, showcaseId] }, }); }); }, [isLoaded, isPanelOpen, seen, showcaseId, updateAndSaveConfig, seenFeatures]); ``` All type checks pass ✓ ## Complete Refactoring Results Transformed the feature showcase system from complex abstractions to simple, direct code: - ✅ Converted to lib/modals pattern (NiceModal) - ✅ Eliminated 2 unnecessary hooks (169 lines removed) - ✅ Removed unnecessary timeout and cancellation logic - ✅ Final result: **10 lines of clear, direct code** that shows dialog and saves config * Cleanup script changes for task attempt dd42978a-2a1a-44bd-9e2e-aa0c6575d236 * hide and save properly --- .../dialogs/global/FeatureShowcaseDialog.tsx | 146 ++++++++++++++ .../showcase/FeatureShowcaseModal.tsx | 179 ------------------ frontend/src/hooks/useShowcasePersistence.ts | 45 ----- frontend/src/hooks/useShowcaseTrigger.ts | 111 ----------- frontend/src/pages/project-tasks.tsx | 36 +++- 5 files changed, 172 insertions(+), 345 deletions(-) create mode 100644 frontend/src/components/dialogs/global/FeatureShowcaseDialog.tsx delete mode 100644 frontend/src/components/showcase/FeatureShowcaseModal.tsx delete mode 100644 frontend/src/hooks/useShowcasePersistence.ts delete mode 100644 frontend/src/hooks/useShowcaseTrigger.ts diff --git a/frontend/src/components/dialogs/global/FeatureShowcaseDialog.tsx b/frontend/src/components/dialogs/global/FeatureShowcaseDialog.tsx new file mode 100644 index 00000000..c32b4d48 --- /dev/null +++ b/frontend/src/components/dialogs/global/FeatureShowcaseDialog.tsx @@ -0,0 +1,146 @@ +import { useState } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { ChevronRight, ChevronLeft } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; +import NiceModal, { useModal } from '@ebay/nice-modal-react'; +import { defineModal } from '@/lib/modals'; +import { Dialog, DialogContent } from '@/components/ui/dialog'; +import { ShowcaseStageMedia } from '@/components/showcase/ShowcaseStageMedia'; +import type { ShowcaseConfig } from '@/types/showcase'; + +interface FeatureShowcaseDialogProps { + config: ShowcaseConfig; +} + +/** + * FeatureShowcaseDialog - Generic multi-stage modal for showcasing features with media + * + * Displays a modal with stages containing videos or images, title, description, + * and navigation controls. ESC key is disabled; only Next/Finish buttons dismiss. + * + * Features: + * - Multi-stage or single-stage support (hides navigation if 1 stage) + * - Video support with loading states and progress bars + * - Image support with loading skeleton + * - Responsive design (full-width on mobile, 2/3 width on desktop) + * - i18n support via translation keys + * - Smooth transitions between stages + * + * Usage: + * ```ts + * FeatureShowcaseDialog.show({ config: showcases.taskPanel }); + * ``` + */ +const FeatureShowcaseDialogImpl = NiceModal.create( + ({ config }: FeatureShowcaseDialogProps) => { + const modal = useModal(); + const [currentStage, setCurrentStage] = useState(0); + const { t } = useTranslation('tasks'); + + const stage = config.stages[currentStage]; + const totalStages = config.stages.length; + + const handleNext = () => { + setCurrentStage((prev) => { + if (prev >= totalStages - 1) { + modal.resolve(); + return prev; + } + return prev + 1; + }); + }; + + const handlePrevious = () => { + setCurrentStage((prev) => Math.max(prev - 1, 0)); + }; + + const handleClose = () => { + modal.hide(); + modal.resolve(); + modal.remove(); + }; + + return ( + { + if (!open) { + handleClose(); + } + }} + uncloseable + className="max-w-none xl:max-w-[min(66.66vw,calc((100svh-20rem)*1.6))] p-0 overflow-hidden" + > + + + + + +
+
+
+

+ {t(stage.titleKey)} +

+
+
+ {currentStage + 1} / {totalStages} +
+
+ +

+ {t(stage.descriptionKey)} +

+ +
+ {Array.from({ length: totalStages }).map((_, index) => ( +
+ ))} +
+ + {totalStages > 1 && ( +
+ {currentStage > 0 && ( + + )} + +
+ )} +
+ + + +
+ ); + } +); + +export const FeatureShowcaseDialog = defineModal< + FeatureShowcaseDialogProps, + void +>(FeatureShowcaseDialogImpl); diff --git a/frontend/src/components/showcase/FeatureShowcaseModal.tsx b/frontend/src/components/showcase/FeatureShowcaseModal.tsx deleted file mode 100644 index ad40ba98..00000000 --- a/frontend/src/components/showcase/FeatureShowcaseModal.tsx +++ /dev/null @@ -1,179 +0,0 @@ -import { useEffect, useState, useRef } from 'react'; -import { motion, AnimatePresence } from 'framer-motion'; -import { ChevronRight, ChevronLeft } from 'lucide-react'; -import { useTranslation } from 'react-i18next'; -import { useHotkeysContext } from 'react-hotkeys-hook'; -import { useKeyExit, Scope } from '@/keyboard'; -import { ShowcaseStageMedia } from './ShowcaseStageMedia'; -import type { ShowcaseConfig } from '@/types/showcase'; - -interface FeatureShowcaseModalProps { - isOpen: boolean; - onClose: () => void; - config: ShowcaseConfig; -} - -/** - * FeatureShowcaseModal - Generic multi-stage modal for showcasing features with media - * - * Displays a bottom-aligned modal with stages containing videos or images, title, description, - * and navigation controls. Properly manages keyboard shortcuts (ESC captured but disabled) - * and scopes to prevent closing underlying features. - * - * Features: - * - Multi-stage or single-stage support (hides navigation if 1 stage) - * - Video support with loading states and progress bars - * - Image support with loading skeleton - * - Responsive design (full-width on mobile, 2/3 width on desktop) - * - i18n support via translation keys - * - Smooth transitions between stages - * - * @param isOpen - Controls modal visibility - * @param onClose - Called when user finishes the showcase (via Finish button on last stage) - * @param config - ShowcaseConfig object defining stages, media, and translation keys - */ -export function FeatureShowcaseModal({ - isOpen, - onClose, - config, -}: FeatureShowcaseModalProps) { - const [currentStage, setCurrentStage] = useState(0); - const { t } = useTranslation('tasks'); - const { enableScope, disableScope, activeScopes } = useHotkeysContext(); - const previousScopesRef = useRef([]); - - const stage = config.stages[currentStage]; - const totalStages = config.stages.length; - - /** - * Scope management for keyboard shortcuts: - * When showcase opens, we capture all currently active scopes, disable them, - * and enable only DIALOG scope. This ensures ESC key presses are captured by - * our showcase handler (which does nothing) instead of triggering underlying - * close handlers. When closing, we restore the original scopes. - */ - useEffect(() => { - if (isOpen) { - previousScopesRef.current = activeScopes; - activeScopes.forEach((scope) => disableScope(scope)); - enableScope(Scope.DIALOG); - } else { - disableScope(Scope.DIALOG); - previousScopesRef.current.forEach((scope) => enableScope(scope)); - } - - return () => { - disableScope(Scope.DIALOG); - previousScopesRef.current.forEach((scope) => enableScope(scope)); - }; - // activeScopes intentionally omitted - we only capture on open, not on every scope change - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isOpen, enableScope, disableScope]); - - useKeyExit( - (e) => { - e?.preventDefault(); - }, - { scope: Scope.DIALOG, enabled: isOpen } - ); - - const handleNext = () => { - setCurrentStage((prev) => { - if (prev >= totalStages - 1) { - onClose(); - return prev; - } - return prev + 1; - }); - }; - - const handlePrevious = () => { - setCurrentStage((prev) => Math.max(prev - 1, 0)); - }; - - return ( - - {isOpen && ( - <> - - - - - - -
-
-
-

- {t(stage.titleKey)} -

-
-
- {currentStage + 1} / {totalStages} -
-
- -

- {t(stage.descriptionKey)} -

- -
- {Array.from({ length: totalStages }).map((_, index) => ( -
- ))} -
- - {totalStages > 1 && ( -
- {currentStage > 0 && ( - - )} - -
- )} -
- - - - - )} - - ); -} diff --git a/frontend/src/hooks/useShowcasePersistence.ts b/frontend/src/hooks/useShowcasePersistence.ts deleted file mode 100644 index e32f5b03..00000000 --- a/frontend/src/hooks/useShowcasePersistence.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { useCallback, useMemo } from 'react'; -import { useUserSystem } from '@/components/config-provider'; - -export interface ShowcasePersistence { - hasSeen: (id: string) => boolean; - markSeen: (id: string) => Promise; - isLoaded: boolean; -} - -export function useShowcasePersistence(): ShowcasePersistence { - const { config, updateAndSaveConfig, loading } = useUserSystem(); - - const seenFeatures = useMemo( - () => config?.showcases?.seen_features ?? [], - [config?.showcases?.seen_features] - ); - - const hasSeen = useCallback( - (id: string): boolean => { - return seenFeatures.includes(id); - }, - [seenFeatures] - ); - - const markSeen = useCallback( - async (id: string): Promise => { - if (seenFeatures.includes(id)) { - return; - } - - await updateAndSaveConfig({ - showcases: { - seen_features: [...seenFeatures, id], - }, - }); - }, - [seenFeatures, updateAndSaveConfig] - ); - - return { - hasSeen, - markSeen, - isLoaded: !loading, - }; -} diff --git a/frontend/src/hooks/useShowcaseTrigger.ts b/frontend/src/hooks/useShowcaseTrigger.ts deleted file mode 100644 index 6f8fed4b..00000000 --- a/frontend/src/hooks/useShowcaseTrigger.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { useState, useEffect, useCallback, useRef } from 'react'; -import type { ShowcaseConfig } from '@/types/showcase'; -import { useShowcasePersistence } from './useShowcasePersistence'; - -export interface ShowcaseTriggerOptions { - enabled: boolean; - openDelay?: number; - resetOnDisable?: boolean; - markSeenOnClose?: boolean; -} - -export interface ShowcaseTriggerResult { - isOpen: boolean; - open: () => void; - close: () => void; - hasSeen: boolean; -} - -export function useShowcaseTrigger( - config: ShowcaseConfig, - options: ShowcaseTriggerOptions -): ShowcaseTriggerResult { - const { - enabled, - openDelay = 300, - resetOnDisable = true, - markSeenOnClose = true, - } = options; - - const persistence = useShowcasePersistence(); - const [isOpen, setIsOpen] = useState(false); - const [hasSeenState, setHasSeenState] = useState(false); - const timerRef = useRef(null); - const mountedRef = useRef(true); - - // Keep 'hasSeenState' in sync if id change or config loads - useEffect(() => { - if (!persistence.isLoaded) return; - setHasSeenState(persistence.hasSeen(config.id)); - }, [persistence.isLoaded, config.id, persistence]); - - // Cleanup timers - useEffect(() => { - mountedRef.current = true; - return () => { - mountedRef.current = false; - if (timerRef.current !== null) { - clearTimeout(timerRef.current); - timerRef.current = null; - } - }; - }, []); - - // Handle enabled state changes - useEffect(() => { - if (!persistence.isLoaded) return; - - if (enabled) { - // Only show if not seen - if (!hasSeenState) { - // Clear any existing timer - if (timerRef.current !== null) { - clearTimeout(timerRef.current); - } - - // Delay opening to ensure UI is mounted - timerRef.current = window.setTimeout(() => { - if (mountedRef.current) { - setIsOpen(true); - timerRef.current = null; - } - }, openDelay); - } - } else { - // Reset when disabled (if configured) - if (resetOnDisable) { - // Clear pending timer - if (timerRef.current !== null) { - clearTimeout(timerRef.current); - timerRef.current = null; - } - setIsOpen(false); - } - } - - return () => { - if (timerRef.current !== null) { - clearTimeout(timerRef.current); - timerRef.current = null; - } - }; - }, [persistence.isLoaded, enabled, hasSeenState, openDelay, resetOnDisable]); - - const open = useCallback(() => { - setIsOpen(true); - }, []); - - const close = useCallback(() => { - if (markSeenOnClose) { - persistence.markSeen(config.id); - setHasSeenState(true); - } - if (timerRef.current !== null) { - clearTimeout(timerRef.current); - timerRef.current = null; - } - setIsOpen(false); - }, [config.id, markSeenOnClose, persistence]); - - return { isOpen, open, close, hasSeen: hasSeenState }; -} diff --git a/frontend/src/pages/project-tasks.tsx b/frontend/src/pages/project-tasks.tsx index dca51cb1..b77b0856 100644 --- a/frontend/src/pages/project-tasks.tsx +++ b/frontend/src/pages/project-tasks.tsx @@ -8,9 +8,9 @@ import { Loader } from '@/components/ui/loader'; import { tasksApi } from '@/lib/api'; import type { GitBranch, TaskAttempt, BranchStatus } from 'shared/types'; import { openTaskForm } from '@/lib/openTaskForm'; -import { FeatureShowcaseModal } from '@/components/showcase/FeatureShowcaseModal'; +import { FeatureShowcaseDialog } from '@/components/dialogs/global/FeatureShowcaseDialog'; import { showcases } from '@/config/showcases'; -import { useShowcaseTrigger } from '@/hooks/useShowcaseTrigger'; +import { useUserSystem } from '@/components/config-provider'; import { usePostHog } from 'posthog-js/react'; import { useSearch } from '@/contexts/search-context'; @@ -201,10 +201,31 @@ export function ProjectTasks() { const isSharedPanelOpen = Boolean(selectedSharedTask); const isPanelOpen = isTaskPanelOpen || isSharedPanelOpen; - const { isOpen: showTaskPanelShowcase, close: closeTaskPanelShowcase } = - useShowcaseTrigger(showcases.taskPanel, { - enabled: isPanelOpen, + const { config, updateAndSaveConfig, loading } = useUserSystem(); + + const isLoaded = !loading; + const showcaseId = showcases.taskPanel.id; + const seenFeatures = config?.showcases?.seen_features ?? []; + const seen = isLoaded && seenFeatures.includes(showcaseId); + + useEffect(() => { + if (!isLoaded || !isPanelOpen || seen) return; + + FeatureShowcaseDialog.show({ config: showcases.taskPanel }).finally(() => { + FeatureShowcaseDialog.hide(); + if (seenFeatures.includes(showcaseId)) return; + void updateAndSaveConfig({ + showcases: { seen_features: [...seenFeatures, showcaseId] }, + }); }); + }, [ + isLoaded, + isPanelOpen, + seen, + showcaseId, + updateAndSaveConfig, + seenFeatures, + ]); const isLatest = attemptId === 'latest'; const { data: attempts = [], isLoading: isAttemptsLoading } = useTaskAttempts( @@ -1049,11 +1070,6 @@ export function ProjectTasks() { )}
{attemptArea}
-
); }