use existing config system instead of local storage for showcase tracking (vibe-kanban) (#1049)
* Perfect! All tasks completed successfully. Here's what was implemented: ## Summary ✅ **Migrated showcase persistence from localStorage to v8 config system** **Backend (Rust):** - Created [v8.rs](file:///private/var/folders/5q/5vgq75y92dz0k7n62z93299r0000gn/T/vibe-kanban/worktrees/0960-use-existing-con/crates/services/src/services/config/versions/v8.rs) with `ShowcaseState` struct tracking `seen_versions: HashMap<String, u32>` - Implemented migration from v7→v8 with default empty showcases - Wired up v8 as active config version **Frontend (TypeScript):** - Created [useShowcasePersistence.ts](file:///private/var/folders/5q/5vgq75y92dz0k7n62z93299r0000gn/T/vibe-kanban/worktrees/0960-use-existing-con/frontend/src/hooks/useShowcasePersistence.ts) hook using config API - Refactored [useShowcaseTrigger.ts](file:///private/var/folders/5q/5vgq75y92dz0k7n62z93299r0000gn/T/vibe-kanban/worktrees/0960-use-existing-con/frontend/src/hooks/useShowcaseTrigger.ts) to use new persistence (kept API unchanged) - Deleted old localStorage utility - Generated TypeScript types including `ShowcaseState` **Key Features:** - Showcase state persists to config file (survives random port changes) - Tracks highest version seen per showcase ID - Optimistic updates for responsive UX - Automatic v7→v8 migration on startup * Cleanup script changes for task attempt 096017a4-28dd-4092-a3ef-7180232ee7d2 * Done! Now `default_git_branch_prefix` is imported from v7 instead of being duplicated. * Cleanup script changes for task attempt 096017a4-28dd-4092-a3ef-7180232ee7d2 * fix FeatureShowcaseModal race condition (vibe-kanban) (#1050) * Done! Collapsed v8 back into v7. The `showcases` field is now part of v7 with `#[serde(default)]`, making it backward compatible without needing a version bump. * Cleanup script changes for task attempt 096017a4-28dd-4092-a3ef-7180232ee7d2 * Fixed! ✅ * showcase simplification (vibe-kanban 94972ea4) frontend/src/config/showcases.ts I want to make it harder to make mistakes here, lets use the type system to our advantage. 1. We could forget to update the `export const showcases` variable, so instead of expecting `taskPanelShowcase`, we should move showcases to all be under one object and only access them through that object. 2. The versioning system is overkill, instead of a `showcases` map with `seen_versions` as another map, we can have a `seen_features` set (realistically an array in JSON) that we insert keys into.
This commit is contained in:
committed by
GitHub
parent
e4a4c004da
commit
50d285cfe5
@@ -78,17 +78,17 @@ export function FeatureShowcaseModal({
|
||||
);
|
||||
|
||||
const handleNext = () => {
|
||||
if (currentStage < totalStages - 1) {
|
||||
setCurrentStage((prev) => prev + 1);
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
setCurrentStage((prev) => {
|
||||
if (prev >= totalStages - 1) {
|
||||
onClose();
|
||||
return prev;
|
||||
}
|
||||
return prev + 1;
|
||||
});
|
||||
};
|
||||
|
||||
const handlePrevious = () => {
|
||||
if (currentStage > 0) {
|
||||
setCurrentStage((prev) => prev - 1);
|
||||
}
|
||||
setCurrentStage((prev) => Math.max(prev - 1, 0));
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,44 +1,41 @@
|
||||
import { ShowcaseConfig } from '@/types/showcase';
|
||||
|
||||
export const taskPanelShowcase: ShowcaseConfig = {
|
||||
id: 'task-panel-onboarding',
|
||||
version: 1,
|
||||
stages: [
|
||||
{
|
||||
titleKey: 'showcases.taskPanel.companion.title',
|
||||
descriptionKey: 'showcases.taskPanel.companion.description',
|
||||
media: {
|
||||
type: 'video',
|
||||
src: 'https://vkcdn.britannio.dev/showcase/flat-task-panel/vk-onb-companion-demo-3.mp4',
|
||||
},
|
||||
},
|
||||
{
|
||||
titleKey: 'showcases.taskPanel.installation.title',
|
||||
descriptionKey: 'showcases.taskPanel.installation.description',
|
||||
media: {
|
||||
type: 'video',
|
||||
src: 'https://vkcdn.britannio.dev/showcase/flat-task-panel/vk-onb-install-companion-3.mp4',
|
||||
},
|
||||
},
|
||||
{
|
||||
titleKey: 'showcases.taskPanel.codeReview.title',
|
||||
descriptionKey: 'showcases.taskPanel.codeReview.description',
|
||||
media: {
|
||||
type: 'video',
|
||||
src: 'https://vkcdn.britannio.dev/showcase/flat-task-panel/vk-onb-code-review-3.mp4',
|
||||
},
|
||||
},
|
||||
{
|
||||
titleKey: 'showcases.taskPanel.pullRequest.title',
|
||||
descriptionKey: 'showcases.taskPanel.pullRequest.description',
|
||||
media: {
|
||||
type: 'video',
|
||||
src: 'https://vkcdn.britannio.dev/showcase/flat-task-panel/vk-onb-git-pr-3.mp4',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const showcases = {
|
||||
taskPanel: taskPanelShowcase,
|
||||
};
|
||||
taskPanel: {
|
||||
id: 'task-panel-onboarding',
|
||||
stages: [
|
||||
{
|
||||
titleKey: 'showcases.taskPanel.companion.title',
|
||||
descriptionKey: 'showcases.taskPanel.companion.description',
|
||||
media: {
|
||||
type: 'video',
|
||||
src: 'https://vkcdn.britannio.dev/showcase/flat-task-panel/vk-onb-companion-demo-3.mp4',
|
||||
},
|
||||
},
|
||||
{
|
||||
titleKey: 'showcases.taskPanel.installation.title',
|
||||
descriptionKey: 'showcases.taskPanel.installation.description',
|
||||
media: {
|
||||
type: 'video',
|
||||
src: 'https://vkcdn.britannio.dev/showcase/flat-task-panel/vk-onb-install-companion-3.mp4',
|
||||
},
|
||||
},
|
||||
{
|
||||
titleKey: 'showcases.taskPanel.codeReview.title',
|
||||
descriptionKey: 'showcases.taskPanel.codeReview.description',
|
||||
media: {
|
||||
type: 'video',
|
||||
src: 'https://vkcdn.britannio.dev/showcase/flat-task-panel/vk-onb-code-review-3.mp4',
|
||||
},
|
||||
},
|
||||
{
|
||||
titleKey: 'showcases.taskPanel.pullRequest.title',
|
||||
descriptionKey: 'showcases.taskPanel.pullRequest.description',
|
||||
media: {
|
||||
type: 'video',
|
||||
src: 'https://vkcdn.britannio.dev/showcase/flat-task-panel/vk-onb-git-pr-3.mp4',
|
||||
},
|
||||
},
|
||||
],
|
||||
} satisfies ShowcaseConfig,
|
||||
} as const;
|
||||
|
||||
42
frontend/src/hooks/useShowcasePersistence.ts
Normal file
42
frontend/src/hooks/useShowcasePersistence.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useUserSystem } from '@/components/config-provider';
|
||||
|
||||
export interface ShowcasePersistence {
|
||||
hasSeen: (id: string) => boolean;
|
||||
markSeen: (id: string) => Promise<void>;
|
||||
isLoaded: boolean;
|
||||
}
|
||||
|
||||
export function useShowcasePersistence(): ShowcasePersistence {
|
||||
const { config, updateAndSaveConfig, loading } = useUserSystem();
|
||||
|
||||
const seenFeatures = config?.showcases?.seen_features ?? [];
|
||||
|
||||
const hasSeen = useCallback(
|
||||
(id: string): boolean => {
|
||||
return seenFeatures.includes(id);
|
||||
},
|
||||
[seenFeatures]
|
||||
);
|
||||
|
||||
const markSeen = useCallback(
|
||||
async (id: string): Promise<void> => {
|
||||
if (seenFeatures.includes(id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await updateAndSaveConfig({
|
||||
showcases: {
|
||||
seen_features: [...seenFeatures, id],
|
||||
},
|
||||
});
|
||||
},
|
||||
[seenFeatures, updateAndSaveConfig]
|
||||
);
|
||||
|
||||
return {
|
||||
hasSeen,
|
||||
markSeen,
|
||||
isLoaded: !loading,
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import type { ShowcaseConfig } from '@/types/showcase';
|
||||
import { hasSeen as hasSeenUtil, markSeen } from '@/utils/showcasePersistence';
|
||||
import { useShowcasePersistence } from './useShowcasePersistence';
|
||||
|
||||
export interface ShowcaseTriggerOptions {
|
||||
enabled: boolean;
|
||||
@@ -27,17 +27,17 @@ export function useShowcaseTrigger(
|
||||
markSeenOnClose = true,
|
||||
} = options;
|
||||
|
||||
const persistence = useShowcasePersistence();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [hasSeen, setHasSeen] = useState(() =>
|
||||
hasSeenUtil(config.id, config.version)
|
||||
);
|
||||
const [hasSeenState, setHasSeenState] = useState(false);
|
||||
const timerRef = useRef<number | null>(null);
|
||||
const mountedRef = useRef(true);
|
||||
|
||||
// Keep 'hasSeen' in sync if id/version change
|
||||
// Keep 'hasSeenState' in sync if id change or config loads
|
||||
useEffect(() => {
|
||||
setHasSeen(hasSeenUtil(config.id, config.version));
|
||||
}, [config.id, config.version]);
|
||||
if (!persistence.isLoaded) return;
|
||||
setHasSeenState(persistence.hasSeen(config.id));
|
||||
}, [persistence.isLoaded, config.id, persistence]);
|
||||
|
||||
// Cleanup timers
|
||||
useEffect(() => {
|
||||
@@ -53,9 +53,11 @@ export function useShowcaseTrigger(
|
||||
|
||||
// Handle enabled state changes
|
||||
useEffect(() => {
|
||||
if (!persistence.isLoaded) return;
|
||||
|
||||
if (enabled) {
|
||||
// Only show if not seen
|
||||
if (!hasSeen) {
|
||||
if (!hasSeenState) {
|
||||
// Clear any existing timer
|
||||
if (timerRef.current !== null) {
|
||||
clearTimeout(timerRef.current);
|
||||
@@ -87,7 +89,7 @@ export function useShowcaseTrigger(
|
||||
timerRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [enabled, hasSeen, openDelay, resetOnDisable]);
|
||||
}, [persistence.isLoaded, enabled, hasSeenState, openDelay, resetOnDisable]);
|
||||
|
||||
const open = useCallback(() => {
|
||||
setIsOpen(true);
|
||||
@@ -95,15 +97,15 @@ export function useShowcaseTrigger(
|
||||
|
||||
const close = useCallback(() => {
|
||||
if (markSeenOnClose) {
|
||||
markSeen(config.id, config.version);
|
||||
setHasSeen(true);
|
||||
persistence.markSeen(config.id);
|
||||
setHasSeenState(true);
|
||||
}
|
||||
if (timerRef.current !== null) {
|
||||
clearTimeout(timerRef.current);
|
||||
timerRef.current = null;
|
||||
}
|
||||
setIsOpen(false);
|
||||
}, [config.id, config.version, markSeenOnClose]);
|
||||
}, [config.id, markSeenOnClose, persistence]);
|
||||
|
||||
return { isOpen, open, close, hasSeen };
|
||||
return { isOpen, open, close, hasSeen: hasSeenState };
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { tasksApi } from '@/lib/api';
|
||||
import type { GitBranch } from 'shared/types';
|
||||
import { openTaskForm } from '@/lib/openTaskForm';
|
||||
import { FeatureShowcaseModal } from '@/components/showcase/FeatureShowcaseModal';
|
||||
import { taskPanelShowcase } from '@/config/showcases';
|
||||
import { showcases } from '@/config/showcases';
|
||||
import { useShowcaseTrigger } from '@/hooks/useShowcaseTrigger';
|
||||
|
||||
import { useSearch } from '@/contexts/search-context';
|
||||
@@ -157,7 +157,7 @@ export function ProjectTasks() {
|
||||
const isPanelOpen = Boolean(taskId && selectedTask);
|
||||
|
||||
const { isOpen: showTaskPanelShowcase, close: closeTaskPanelShowcase } =
|
||||
useShowcaseTrigger(taskPanelShowcase, {
|
||||
useShowcaseTrigger(showcases.taskPanel, {
|
||||
enabled: isPanelOpen,
|
||||
});
|
||||
|
||||
@@ -766,7 +766,7 @@ export function ProjectTasks() {
|
||||
<FeatureShowcaseModal
|
||||
isOpen={showTaskPanelShowcase}
|
||||
onClose={closeTaskPanelShowcase}
|
||||
config={taskPanelShowcase}
|
||||
config={showcases.taskPanel}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -13,6 +13,5 @@ export interface ShowcaseStage {
|
||||
|
||||
export interface ShowcaseConfig {
|
||||
id: string;
|
||||
version: number;
|
||||
stages: ShowcaseStage[];
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
/**
|
||||
* Check if a user has seen a specific showcase version
|
||||
*
|
||||
* @param id - Unique identifier for the showcase
|
||||
* @param version - Version number for the showcase
|
||||
* @returns true if the user has seen this showcase version
|
||||
*
|
||||
* Storage key format: `showcase:{id}:v{version}:seen`
|
||||
*/
|
||||
export function hasSeen(id: string, version: number): boolean {
|
||||
const key = `showcase:${id}:v${version}:seen`;
|
||||
return localStorage.getItem(key) === 'true';
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a showcase as seen
|
||||
*
|
||||
* @param id - Unique identifier for the showcase
|
||||
* @param version - Version number for the showcase
|
||||
*
|
||||
* Storage key format: `showcase:{id}:v{version}:seen`
|
||||
*/
|
||||
export function markSeen(id: string, version: number): void {
|
||||
const key = `showcase:${id}:v${version}:seen`;
|
||||
localStorage.setItem(key, 'true');
|
||||
}
|
||||
Reference in New Issue
Block a user