fix(FN-1987): move skills search to top and improve catalog fallback
- Move the shared skills search input to the top of SkillsView so it applies to discovered skills and catalog results - Add client-side discovered skills filtering by name/path with a dedicated filtered-empty-state message - Extend skills adapter fallback logic to use unauthenticated catalog search when authenticated requests return 400, 401, or 403 - Add dashboard component and adapter tests covering discovered filtering behavior and authenticated-endpoint fallback scenarios
This commit is contained in:
@@ -30,6 +30,15 @@ export function SkillsView({ projectId, addToast, onClose }: SkillsViewProps) {
|
||||
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const [debouncedQuery, setDebouncedQuery] = useState("");
|
||||
|
||||
// Client-side filtering for discovered skills
|
||||
const filteredDiscoveredSkills = searchQuery.trim()
|
||||
? discoveredSkills.filter(
|
||||
(s) =>
|
||||
s.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
s.relativePath.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
)
|
||||
: discoveredSkills;
|
||||
|
||||
// Fetch discovered skills
|
||||
const loadDiscoveredSkills = useCallback(async () => {
|
||||
setIsLoadingDiscovered(true);
|
||||
@@ -152,6 +161,18 @@ export function SkillsView({ projectId, addToast, onClose }: SkillsViewProps) {
|
||||
|
||||
{/* Scrollable content area */}
|
||||
<div className="skills-view-content">
|
||||
{/* Search — at top for both sections */}
|
||||
<div className="skills-view-search">
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
placeholder="Search skills..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => handleSearchChange(e.target.value)}
|
||||
aria-label="Search skills"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Discovered Skills Section */}
|
||||
<section className="skills-view-section" aria-labelledby="discovered-skills-title">
|
||||
<h3 id="discovered-skills-title" className="skills-view-section-title">
|
||||
@@ -167,9 +188,13 @@ export function SkillsView({ projectId, addToast, onClose }: SkillsViewProps) {
|
||||
<div className="skills-view-empty">
|
||||
<p>No skills discovered in this project.</p>
|
||||
</div>
|
||||
) : filteredDiscoveredSkills.length === 0 ? (
|
||||
<div className="skills-view-empty">
|
||||
<p>No discovered skills match your search.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="skills-view-list">
|
||||
{discoveredSkills.map((skill) => (
|
||||
{filteredDiscoveredSkills.map((skill) => (
|
||||
<div key={skill.id} className="skills-view-item">
|
||||
<div className="skills-view-item-info">
|
||||
<span className="skills-view-item-name">{skill.name}</span>
|
||||
@@ -198,18 +223,6 @@ export function SkillsView({ projectId, addToast, onClose }: SkillsViewProps) {
|
||||
Skills Catalog
|
||||
</h3>
|
||||
|
||||
{/* Search */}
|
||||
<div className="skills-view-search">
|
||||
<input
|
||||
type="text"
|
||||
className="form-input"
|
||||
placeholder="Search skills..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => handleSearchChange(e.target.value)}
|
||||
aria-label="Search skills catalog"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Catalog Content */}
|
||||
{catalogError ? (
|
||||
<div className="skills-view-error">
|
||||
|
||||
@@ -620,4 +620,74 @@ describe("SkillsView", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("discovered skills filtering", () => {
|
||||
it("filters discovered skills by search query", async () => {
|
||||
render(<SkillsView addToast={mockAddToast} onClose={onClose} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("test-skill")).toBeTruthy();
|
||||
expect(screen.getByText("another-skill")).toBeTruthy();
|
||||
});
|
||||
|
||||
const searchInput = screen.getByPlaceholderText("Search skills...");
|
||||
fireEvent.change(searchInput, { target: { value: "test-skill" } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("test-skill")).toBeTruthy();
|
||||
expect(screen.queryByText("another-skill")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows filtered empty state when no discovered skills match search", async () => {
|
||||
render(<SkillsView addToast={mockAddToast} onClose={onClose} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("test-skill")).toBeTruthy();
|
||||
});
|
||||
|
||||
const searchInput = screen.getByPlaceholderText("Search skills...");
|
||||
fireEvent.change(searchInput, { target: { value: "zzz-nonexistent" } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("No discovered skills match your search.")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows original empty state when no skills are discovered", async () => {
|
||||
mockFetchDiscoveredSkills.mockResolvedValue([]);
|
||||
|
||||
render(<SkillsView addToast={mockAddToast} onClose={onClose} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("No skills discovered in this project.")).toBeTruthy();
|
||||
});
|
||||
|
||||
// Search should not override the "no skills discovered" empty state
|
||||
const searchInput = screen.getByPlaceholderText("Search skills...");
|
||||
fireEvent.change(searchInput, { target: { value: "test" } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("No skills discovered in this project.")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it("filters discovered skills by relativePath", async () => {
|
||||
render(<SkillsView addToast={mockAddToast} onClose={onClose} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("test-skill")).toBeTruthy();
|
||||
expect(screen.getByText("another-skill")).toBeTruthy();
|
||||
});
|
||||
|
||||
// Search by path instead of name
|
||||
const searchInput = screen.getByPlaceholderText("Search skills...");
|
||||
fireEvent.change(searchInput, { target: { value: "another-skill" } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText("test-skill")).toBeNull();
|
||||
expect(screen.getByText("another-skill")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user